阅读 35

理解堆栈执行与闭包

一、名词解释

名词具象化有利于对执行过程理解更透彻。主要有ECGVOAOGO、执行上下文、执行环境栈、this、闭包、原型及原型链、FunctionObject

1、JS执行平台

哪些地方可以执行JS代码?

  • nodejs
  • 浏览器
  • 原生及非原生应用(webview

那么不论我们的JS代码在哪执行,最终都需要提供一个环境。

2、执行环境(以浏览器平台为例)

  1. 编程语言开发的程序在运行的时候,最终都会拿到内存当中完成执行。
  2. JS中,当浏览器加载界面的时候,就会主动的向计算机申请一片内存空间(执行环境栈)
  3. ESC(execution stack context) 执行环境栈

3、执行上下文(不需要对它具象)

首先通过一段代码明确执行上下文存在的意义:

const name = 'zce'
function foo() {
    const name = 'lagou'
}
function fn() {
    console.log('1111')
}
复制代码
  1. 一个JS文件中会包含多种代码(变量、函数、对象),将不同的代码组合在一起形成了代码块。
  2. 不同的代码块之间可能会出现相同的命名(如上)......等,代码在执行的时候如何对它们进行区分呢?
  3. 区分方式:将每段代码都放到自己的执行上下文当中
  4. 我们可以认为执行上下文就类似一个"容器",在它当中包含了当前代码进栈执行时所需要使用的一切
  5. EC(execution context) 执行上下文

4、进栈执行

  1. 执行环境栈,类似大容器(浏览器加载界面默认会创建内存空间),先进后出
  2. 代码执行(函数调用)会产生一个独立的执行上下文,它会进到环境栈当中,运行内部的代码
  3. 内部代码执行完成后,考虑是否出栈

5、EC(G)全局执行上下文

  1. 默认情况下浏览器会生成一个全局执行上下文用于存放。
  2. 默认情况下它会自动出现在栈(执行环境栈)底。那什么时候这个执行上下文占据的内存空间会被释放呢?(关闭当前网页,因为浏览器是多线程的,当前网页只是其中的一个线程)

6、VO(G)全局变量对象

  1. 全局变量对象(它是一个对象,一定可以存数据,存什么?),VO(G)EC(G)全局执行上下文中。

7、GO全局对象

  1. global object全局对象,它和VO(variable object)并不是同一个东西
  2. 浏览器在加载界面的时候默认会创建一个空间,它里面存放了一些我们在JS当中可以直接访问的APIsetTimeoutsetIntervaltoString......)
  3. 为了JS方便找到内置的这些API,在全局执行上下文当中的VO中定义了一个变量叫window(引用地址)指向这些API

8、声明

如:var name; const age

9、定义

如:name = 'aaa'

二、JS数据类型的表现形式

  1. 基本数据类型值存在栈空间,引用类型需要单独开辟一个空间来进行储存(堆内存)
  2. 堆内存会有一个内存地址(16进制的地址),在栈区的变量存放的是内存地址
  3. 作用域链查找机制,最终到( GO
  4. 函数的讨论一般分为2部分:函数的创建 + 函数的执行
  5. 函数的创建和变量有些类似,可以将函数名看作一个变量,但是它与变量的不同点在于,提升阶段(预解析)函数是声明 + 定义都做,而变量只做声明。
  6. 函数也是一个对象,因此会占据一个堆内存。
  7. 对于函数来说,在创建的时候就会确定好它的作用域 (词法作用域、scope)
  8. 函数所对应的堆内存当中会将函数体以字符串的方式保存起来。
  9. 函数执行的目的就是为了将它里面所保存的字符串形式的代码拿到内存中,当作真正的代码运行起来。
  10. 因为函数里面也有很多的数据,为了和其他的代码块当中的数据做隔离,因此每个函数每次调用都会创建一个新的独立的执行上下文。 在这个上下文中保存它内部的数据,然后再整体执行进栈调用。

三、函数的执行

函数执行,需要走的6个步骤:

  1. 确定作用域链:函数当前执行上下文(如:EC(FOO))--> 函数的词法作用域(EC(G)
  2. 确定this: 如window...(后续讨论)
  3. 初始化argument:创建一个伪数组...
  4. 形参赋值:形参赋值就会在AO当中新增一个变量
  5. 变量提升
  6. 代码执行: 在代码执行中可能会开辟新的堆内存、赋值、创建函数等等

图解一个实例,便于理解函数执行: image.png

四、闭包机制

闭包是一种机制,代码只是具体的表现形式,例如我们我们常说的大函数嵌套小函数,再返回一个小函数。它主要起到作用是对数据进行保护和保存。
解析:函数执行时会产新生成一个执行上下文,一般来说函数中的代码执行结束之后就需要出栈从而释放当前上下文所占据的内存空间,从而释放它内部的声明和值,但是如果此时当前执行上下文当中的数据(一般就是堆内存引用)被当前上下文之外的变量所引用,那么这个上下文就不能被释放掉,此时就形成了一个闭包。

var zce = 100
function fn() {
  var zce = 200
  return function (a) {
    console.log(a + zce++)
  }
}

var foo = fn()
foo(10)
foo(20)
复制代码

图解以上代码的执行过程: image.png 分析代码中闭包:闭包的好处就是可以对一些数据进行保存,例如下文中的zce,函数内部的zce和全局的zce互不干扰,同时闭包还可以保存数据,例如 0x001 所对应的内存空间,本该在FN执行结束后释放掉,但是由于 EC(G)当中的foo对其有引用,所以让它可以在后续代码中继续被使用。

五、闭包与垃圾回收

  • 上述代码运行可以发现,代码的运行是需要内存空间的。无论是栈内存还是堆内存都属于计算机内存空间
  • 内存空间大小是有上限的,因此不能无限制使用,所以需要内存管理,也就是垃圾回收

六、闭包练习

分析以下代码的执行过程:

var a = 10
function foo (a) {
    return function (b) {
        console.log(b + (++a))
    }
}
var fn = foo(10)
fn(5)
foo(6)(7)
fn(20)
console.log(a)
复制代码

图解代码的堆栈执行情况: image.png

文章分类
前端
文章标签