0、代码:
从下面这个面试题代码详解JavaScript在堆栈中的执行过程
let a = 10
function fn1(b) {
let a = 2
function fn2(c) {
console.log(a + b + c)
}
return fn2
}
let fn3 = fn1(2)
fn3(3)
简述JavaScript代码在堆栈中的执行过程:
- JavaScript代码在开始执行之后,会在堆内存中创建执行环境栈,用它来存放不同的执行上下文
- 代码从上向下开始执行,最先创建的是ECG全局执行上下文
- 在这个ECG里面,将全局作用域下的代码进行声明和存放
- 其中基本类型值存放在栈里面,引用类型值存放在堆里面
- 堆里面的数据一般由GC进行处理,栈里面的数据由JavaScript主线程进行管理
- 在执行过程中,每当遇到函数执行时,就会再生成一个执行上下文进栈
- 代码执行完成以后,根据是否产生闭包来决定当前上下文引用的堆数据是否进行释放
一些名词解释:
- ECS: Execution Context Stack,执行环境栈,遵守先进后出原则
- EC(G):全局执行上下文压,只创建一次,只有一个,页面关闭时才会释放掉
- EC(xx): xx函数私有执行上下文,根据是否产生闭包来决定当前上下文引用的堆数据是否进行释放
- VO: Variable Object,变量对象,用来保存当前执行上下文中所有变量的一个空间
- AO: Active Object,私有变量对象,用来保存当前私有上下文中所有的变量的一个空间
- GO: Global Object,全局对象,是预定义的对象 【通过window可以调取所有内置的属性和方法】
1、执行过程详细描述:
- 代码开始执行时,创建一个执行环境栈【ECStack】,用于存放执行上下文
- 在执行第一行代码前,需要先创建一个全局的执行上下文 EC(G)
- 并且初始化一些预定义的对象,即GO全局对象
- 然后将这个全局执行上下文压入执行环境栈中
第一步:
- 代码开始从第一行开始执行,let a = 10
- 此时在当前上下文中,初始化一个变量对象VO,存放 a = 10
- a是一个基本类型数值10,所以直接存放在栈内存当中
第二步:
- 代码继续执行,function fn1(b) {}
- 这个地方由于fn1函数是一个引用类型,所以需要开辟一个新的堆空间进行存储,假设地址为add_fn1
- 在add_fn1中,存放着 function fn1(b) { ...... }, name: fn1; length: 1
- 此时,在VO中,存放 fn1 = add_fn1
存放的是fn1函数的地址
- 并且函数在创建中已经拥有自己的作用域
scope
VO
第三步:
- 代码继续执行,let fn3 = fn1(2) ,
- 此时 fn1()函
- 数被调用,所以再开启一个执行上下文 EC(fn1)
- 在 EC(fn1)这个函数执行上下文中,需要做一些事情
- 首先确定 this 指向,当前是非严格形式下的函数对象,所以, this = window
- 然后初始化作用域链,先是自己的作用域,<fn1.ao>
- 往上查找是全局执行上下文 EC(G) 中的作用域
- 此时作用域链初始化完成,然后初始化自己的AO
- 存放 argument: { 0: 2 }; b = 2;
- 继续执行代码, let a = 2
- 这时,在自己的上下文中也有一个变量对象AO 存放 a = 2
- a = 2 是数值类型直接存放在栈内存中
第四步:
- 继续执行代码, function fn2(c) {}
- fn2函数也是一个引用类型,所以再开辟一个新的堆空间进行存储,假设地址为add_fn2
- 在add_fn2中,同样也存放着 function fn2(c) { ...... }, name: fn2, length: 1
- 此时,在AO中,存放 fn2 = add_fn2[存放的是fn2函数的地址]
- 同样,拥有自己的作用域 [[scope]] foo.AO
第五步:
- 代码继续执行, return fn2
- 这个时候全局执行上下文VO中就可以拿到这个add_fn2的存放地址 即 fn3 = add_fn2
- 这个时候由于fn1函数已经执行完毕,内存本应该释放
- 但是在全局执行上下文中,仍然对fn1函数中的fn2, 即 add_fn2有引用
- 形成了闭包,这个时候内存不能被释放掉
- 当前浏览器的机制就会把当前执行栈向下压,将下面的栈提上来执行
第六步:
- 继续执行代码,fn3(3)
- fn3就是add_fn2,即 函数fn2,调用这个函数fn2,会再开辟一个新的执行上下文 EC(fn2)
- 首先确定 this 指向,当前是非严格形式下的函数对象,所以, this = window
- 然后初始化作用域链,先是自己的作用域,<fn2.AO>
- 往上查找是 EC(fn2) 的作用域<fn1.AO>
- 再往上查找是全局执行上下文 EC(G) 的作用域
- 此时作用域链初始化完成,然后初始化自己的AO
- 存放 argument: { 0: 3 }; c = 3;
第七步: 继续执行代码,console.log(a + b + c) a在自己的作用域中是没有的,所以按照作用域链的顺序向上查找,在 EC(fn2)中找到a,所以 a = 2 b在自己的作用域中也是没有的,所以按照作用域链的顺序向上查找,在 EC(fn2)中找到b,所以 b = 2 c 在自己的作用直接有的,直接拿来用 c = 3 所以最终输出 2 + 2 + 3 的结果,7
最后: 这时候我们的代码就全部执行完毕了 栈中的数据就交由JS主线程就行回收【出栈】,堆中的数据就有GC进行回收