JS代码执行过程以及闭包的产生原理

以如下代码为例,详细地展示JS执行代码的过程以及闭包产生的原理JS代码的执行过程以及闭包的产生原理

<span style="background-color:#f8f8f8"><span style="color:#333333">let a = 0;
​
function foo() {
    var c = 100;
​
    function bar() {
         console.log(c);
    }
    return bar
}
var fn = foo();
fn();</span></span>

过程一:解析(Scanner词法解析

  • 1.在该代码运行之前,即代码解析阶段时,jsV8引擎就会在堆内存中创建一个Global Object 全局对象(缩写:GO) (假设内存地址为0x001),里面放的是一些全局函数和对象(这个过程先(早)于全局代码解析时将变量和函数进行绑定添加为GO的属性(后面会讲到变量和函数的绑定))

    <span style="background-color:#f8f8f8">var Global Object = {
        String: ƒ String(),
        Date: f Date(),
        setTimeout: f setTimeout(),
        ...
    }</span>
  • 2.同时,在jsv8引擎内部提供的Execution Context Stack执行上下文栈(简称:ECS)(所有代码的执行都是在执行上下文栈中执行的)中,创建一个Global Execution Context全局执行上下文(GEC),用来运行全局作用域下的代码,GEC包含了两部分

    • 一部分是:Variable Object(简称: VO),它指向Global Object 对象) 和 一个Scope Chain作用域链 (它由本身作用域加上父级作用域组成)

      • VO(Variable Object) : 0x001(Global Object)

      • scope chain: VO+parentScope

    • 另一部分则是包含所要执行的代码

  • 3.在全局代码运行前解析(词法解析Scanner阶段)时,进行词法绑定

    • 1.定义在全局的变量会进行进行绑定,将它们变成Global Object的属性,且它们的值都为undefined

    • 2.定义在全局的函数,既会将函数名变成Global Object的属性,同时又会在堆内存中定义相应的函数对象,并将函数对象所在内存的地址赋值给函数名

      • 如上代码,则会创建一个foo函数对象(假设内存地址为0x00a),将0x00a赋值给函数名。

      • 如果是函数内部的函数,像bar函数,则不会创建相应的函数对象,而是在运行该函数foo的代码前进行词法解析的时候,创建相应的函数对象

    • 3.而该函数对象里面包含着它的[[Scope]]:Parent Scope 父级作用域以及函数体(代码段)

      • 此代码中,他函数对象的父级作用域为 :Global Object (0x001)

过程二(运行代码)

  • 如果是简单的变量赋值,会将Global Object中的变量的值进行更新

    • 如运行第一行代码,则会将Global Object里面的a的值赋值为0

  • 如果是函数,会将foo的函数地址先赋值给Global Object中foo函数名,同时在运行函数内部代码之前(词法解析Scanner阶段):

    • 1.foo函数先会在堆内存中创建一个相应的Activation Object(简称 :AO)(假设地址为:0x002),然后解析代码时,将函数中定义的变量和函数进行类似于Global Object中的操作(词法绑定):

      • 将变量绑定为Activation Object的属性,值赋值为undefined(c : undefined)

      • 函数则是将函数名绑定为Activation Object的属性,同时在堆内存中创建对应的bar函数对象(假设内存地址为0x00b),并将Activation Object里面的函数名赋值为对应的堆内存中函数对象的内存地址 (bar : 0x00b)

      • 堆内存中函数对象里面同样包含包含着它的[[Scope]]:Parent Scope 父级作用域(指向AO:0x00a)以及函数体(代码段)

      • 如上,在代码运行到 var fn = foo()时,先进行函数运行,再进行代码赋值

      <span style="background-color:#f8f8f8">var Activation Object = {
          c : undefined,
          bar : 0x00b
      }</span>
    • 2.然后(慢于AO的创建)在ECS执行上下文栈中创建一个Functional Execution Context函数执行上下文(简称 :FEC),该FEC同样包含两部分:

      • 一部分是是Variable Object,(它指向AO) 和 scoped chain 作用域链 (它又本身作用域加上父级作用域组成)

      • 另外一部分就是执行的代码

    • 3.当Execution Context Stack中的foo Functional Execution Context执行代码时,foo Activation Object也是类似于Global Object的执行代码的相应处理:

      • 如果是变量,则将 Activation Object 中的变量 进行值的赋值(更新),如代码,c = 100;

      • 如果是函数,则又会在堆内存中创建相应的函数对象,并将Activation Object 中的函数名赋值为对应的内存地址

  • 注意:

    • 当函数运行完之后,执行上下文栈ECS会将函数执行产生的函数执行上下文FEC移除栈,如果一个函数中嵌套执行了另外一个函数,那么此时该函数执行上下文还未运行完,即不会被移除栈,同时新创建的函数执行上下文会入栈,此时两个函数执行上下文同时存在

    • 如果两个函数是非嵌套关系(并行),此时他们的函数执行上下文是不会在执行上下文栈中同时出现的,除非是嵌套函数执行

    • 同时只有全局执行上下文不会被移除栈

  • 当函数foo运行完之后,此时GO中fn的值为0x00b(bar函数对象)

    • 当一个函数执行上下文被移除栈,那么Functional Execution Context函数执行上下文(FEC)它就不存在,也就意味着它里面的Variable Object不存在,即AO对象没有人引用它,那么它就成为了垃圾对象了,此时就会被Garbage Collection垃圾回收器回收(GC)

  • 此时再运行fn,即fn()

    • 此时又将新创建一个bar Activation Object(假设内存地址为 :0x003) 和 一个函数执行上下文,依然进行上面函数类似操作

  • 但是当我们执行完fn时,此时我们却发现fn指向了foo的Activation Object,按理说foo 的Activation Object应该成为垃圾对象,此时却没有,这是因为有fn指向了foo的Activation Object,这就是闭包的产生原理

    • 如下:此时我们就发现因为有着GO中fn指向foo 的Activation Object,所以当我在全局作用域下运行bar函数时,任然可以通过bar函数对象中的作用域链找到foo 的Activation Object,打印其中的c的值


 

理解源于coderwhy王红元老师的js高级课程讲述;同时大家可以关注微信公众号“coderwhy”

初次写文章,如有错误,还望指正!

         

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值