美文网首页
javascript是怎么执行的?

javascript是怎么执行的?

作者: miao8862 | 来源:发表于2021-05-26 21:43 被阅读0次

    这篇文章会集合介绍关于js运行过程中的VOAO、形参、实参arguments、执行上下文EC、作用域scope、作用域链scopeChain、闭包等概念,并以画图或代码方式串起来说明js的执行过程。

    • ECStack:执行栈,所有的代码都要放到执行栈中执行
    • EC:excution context,执行上下文,当执行一个块级作用域({}中有let,const,function定义)或函数时,会形成一个新的执行环境,这个环境就被称为执行上下文
    • VO:avarible object,变量对象,存储当前上下文中的变量
    • AO:actived object,活动的变量对象,当函数执行时,VO=>AO,一般函数执行生成的变量对象,称之为AO
    • 实参:函数执行时,实际传给函数的参数集合arguments
    • 形参:在函数定义时,定义的参数名
    • 作用域:所在的执行上下文中的变量对象VO/AO
    • 作用域链:如果查找一个变量,会先在自己的执行上下文的AO对象中查找,如果没有,会到所在的执行上下文中的AO对象中查找,如果再没有,就再找所在的执行上下文的上一级执行上下文中的AO对象中查找,直到找到变量或找到全局VO对象为止,这条链,我们就称之为作用域链
    • 闭包:最常见的说法是内部函数调用外部函数的变量

    js代码执行过程中,会生成一个执行栈Ecstack,所有的代码执行都会在这个栈中进行:

    1. 在执行过程中先在全局创建一个全局执行上下文EC(G),当执行全局代码时,会将当前栈压入执行栈Ecstack,也就是入栈
    2. 当运行到一个{}代码片段或者一个函数,就会创建对应的执行上下文EC(xx),并将其入栈。
    3. 执行栈按照先进后出的特性,执行代码,当执行完当前代码后,会将当前执行上下文销毁,并将其移出执行栈,也就是出栈,依次执行并进行出栈,直至执行栈为空为止。
    4. 如果有些代码执行完后不释放,就会将此栈压入栈的最底层,等待被其它栈调用,这也就是我们常说的闭包

    接下来,会详细说明每个过程中会做到的事:

    函数声明(创建)时

    函数声明(创建)时会做以下两件事:

    • 创建一个堆内存,这个堆内存中会存放:
      1. 代码字符串,即将要执行的代码部分以字符串形式存储
      2. 使用键值对存储方法对象
    • 初始化当前函数的作用域
      当前函数的作用域:[[scope]] = 函数所在的执行上下文中的变量对象VO/AO

    如下,在全局定义一个函数fn,在浏览器中可以看到对象上的键值对信息,因为它是在全局环境下创建的,所以[[scope]]指向的是全局执行上下文中的VO变量对象window

    function fn(x) {
      console.log(x)
    }
    // 比如AAAFFF000堆内存:
    // 1. 代码字符串:
    // "console.log(x)"
    
    // 2. 键值对存储方法对象等:
    // length: 1
    // name: "fn"
    // arguments: null
    // caller: null
    // ...
    
    image.png

    声明对象时

    创建一个堆内存,使用键值对存储对象:

    let a = {
      n: 1
    }
    // 比如,AAAFFF000堆内存:
    // n: 1
    

    函数执行时

    1. 创建一个新的执行上下文EC(函数),并将这个执行上下文压到执行栈ECStack中执行
    2. 初始化this的指向
    3. 初始化作用域链(本质上是一个链表):[[scopeChain]]:当前上下文AO => 所在的上下文AO => 再上一级上下文AO => ... => 直到全局上下文中的VO(G)
    4. 创建AO变量对象用来存储变量:
      a. 初始化实参集合arguments
      b. 创建形参变量并且赋值
      c. 执行代码

    需要注意的是,在非严格模式下,a.b步骤会建立映射关系;在严格模式下则不会。ES6的 箭头函数中没有arguments实参集合

    // 非严格模式
    function fn(x, y) {
      // 1. 实参集合初始化:arguments: {0:10, 1:20}
      // 2. 形参变量和赋值:
      //      x = 10, y = 20
      //      形参映射:x = 10, y = 20 => arguments
      console.log(x, y, arguments); // 10 20 [Arguments] { '0': 10, '1': 20 }
      // 3. arguments[0] = 100 => arguments: {0:100, 1:20} => x = 100
      arguments[0] = 100;
      // 4. y = 200 => arguments: {0:100, 2:200}
      y = 200;
      console.log(x, y, arguments); // 100 200 [Arguments] { '0': 100, '1': 200 }
    }
    fn(10, 20)
    
    // 严格模式
    function fn(x, y) {
      "use strict"
      // 1. 实参集合初始化:arguments: {0:10, 1:20}
      // 2. 形参变量和赋值:
      //      x = 10, y = 20
      //      无形参映射
      console.log(x, y, arguments); // 10 20 [Arguments] { '0': 10, '1': 20 }
      // 3. arguments[0] = 100 => arguments: {0:100, 1:20}
      arguments[0] = 100;
      // 4. y = 200
      y = 200;
      console.log(x, y, arguments); // 10 200 [Arguments] { '0': 100, '1': 20 }
    }
    fn(10, 20)
    

    来几道面试题实践

    1. 360面试题

    // 1. x => BBBFFF000: 
    // 0: 12
    // 1: 23
    // length: 2
    let x = [12, 23]   
    
    // 2. fn => AAAFFF000
    
    function fn(y) {
      // 4. 初始化实参集合: 
      //    x => arguments: [[12,23]] => BBBFFF000(0: 12, 1: 23, length: 2)
      // 5. 创建形参并赋值:
      //    映射:y => x => BBBFFF000(0: 12, 1: 23, length: 2)
      // 6. y => x => BBBFFF000(0: 100, 1: 23, length: 2)
      y[0] = 100
      // 7. 指向一个新的堆内存:CCCFFF000(0: 100, length: 1)
      //     y => [100] => CCCFFF000(0: 100, length: 1)
      y = [100]
      // 8. CCCFFF000之前没有1的索引,所以CCCFFF000添加此索引(0: 100, 1: 200, length: 2)
      //     y => [100, 200] => CCCFFF000(0: 100, 1: 200, length: 2)
      y[1] = 200;
      console.log(y); // [ 100, 200 ]
    }
    // 3. 执行fn
    fn(x)
    console.log(x); // x指向的是arguments,也就是BBBFFF000:[ 100, 23 ]
    

    2. 输出以下结果

    var x = 10
    ~function(x) {
      console.log(x);  // undefined
      x = x || 20 && 30 || 40
      console.log(x);  // 30
    }();
    console.log(x);  // 10
    

    分析:

    1. js代码执行时,会创建一个ECStack执行栈,用于执行代码

    2. 创建全局执行上下文EC(G),并创建其VO对象,有一个全局变量x和自执行函数的堆内存AAAFFF000,并将EC(G)入栈

      创建全局执行上下文
    3. 自执行函数,创建自执行函数的执行上下文EC(自执行函数),并为其创建AO对象,其声明过程,包括实参初始化和形参赋值,因为没有实参,所以实参arguments:{},形参x没有赋值,所以是undefined,并将这个执行上下文入栈

      创建自执行函数的执行上下文
    4. 自执行函数开始执行,console.log(x) => undefined,接着执行x = x || 20 && 30 || 40赋值,这里考察的是运算符的计算和优化级问题:

      • A || B:如果A为真,返回A,否则返回B
      • A && B:如果A为真,返回B,否则返回A
      • 优先级:&&运算符 优先级高于||
        所以x = x || 20 && 30 || 40 => x = undefined || 30 || 40 => x = 30,执行console.log(x) => 30
        执行自执行函数
    5. 自执行函数完成后,将其执行上下文出栈,并销毁


      出栈
    6. 继续执行全局上下文console.log(x),输出10

      image.png
    7. 全局执行上下文EC(G)出栈,清空执行栈

    3. 输出以下结果

    let x = [1, 2], y= [3, 4]
    ~function(x) {
      x.push('a')
      x = x.slice(0)
      x.push('b')
      x = y;
      x.push('c')
      console.log(x, y);   // [ 3, 4, 'c' ] [ 3, 4, 'c' ]
    }(x)
    console.log(x, y);  // [ 1, 2, 'a' ] [ 3, 4, 'c' ]
    

    分析:

    1. 最初,执行栈ECStack中,创建全局执行上下文EC(G)入栈,并开始创建全局变量对象VO(G)
      a. x => AAAFFF000({0: 1, 1: 2, length: 2})
      b. y => AAAFFF111({0:3, 1: 4, length: 2})
      c. 自执行函数=> AAAFFF222
      d. 自执行函数的作用域: [[scope]] = VO(G)
    2. 开始执行自执行函数,生成EC(自执行函数),并将其压入执行栈
    3. 初始化自执行函数的作用域链: [[scopeChain]]:AO(自执行函数) => VO(G)
    4. 开始创建AO对象:
      a. 实参初始化arguments => AAAFFF000({0: 1, 1: 2, length: 2})
      b. 创建形参并赋值,x => arguments => AAAFFF000({0: 1, 1: 2, length: 2})
      c. 开始执行函数的代码串
    5. 执行x.push('a')x => arguments => AAAFFF000({0: 1, 1: 2, 3: 'a', length: 3})
    6. 执行x = x.slice(0),x.slice(0)会创建一个新的堆内存空间,值拷贝x,x => AAAFFF333({0: 1, 1: 2, 3: 'a', length: 3})
    7. 执行x.push('b')EC(自执行函数)中私有x, x => AAAFFF333({0: 1, 1: 2, 3: 'a', 4: 'b', length: 4})
    8. 执行x = y;EC(自执行函数)中因为自己的VO对象中没有私有变量y,所以在其作用域链[[scopeChain]] = <VO(自执行函数),VO(G)>中找向上一级作用域VO(G)中查找y,发现全局VO(G)中有y,所以私有 x => y => AAAFFF111({0:3, 1: 4, length: 2})
    9. 执行x.push('c')x => y => AAAFFF111({0: 3, 1: 4, 3: 'c', length: 3})
    10. 执行console.log(x, y),打印的是EC(自执行函数)中私有变量x和y,此时它们都指向AAAFFF111({0: 3, 1: 4, 3: 'c', length: 3}),所以值为[ 3, 4, 'c' ]
    11. 自执行函数执行完毕,出栈,执行EC(G)中的console.log(x, y),全局中的x => arguments => AAAFFF000({0: 1, 1: 2, 3: 'a', length: 3})y => AAAFFF111({0: 3, 1: 4, 3: 'c', length: 3}),所以输出[ 1, 2, 'a' ] [ 3, 4, 'c' ]
    12. 全局执行上下文中代码执行完成,出栈,此时清空了执行栈

    4. 关于闭包题目

    function A(y) {
      let x = 2;
      function B(z) {
        console.log(x + y + z); // 7
      }
      return B
    }
    let C = A(2)
    C(3)  
    

    分析:

    1. 在执行栈中ECStack,先创建全局执行上下文EC(G) ,并将其入栈,并创建全局变量对象VO(G)A =>AAAFFF000,并初始化其作用域,即指向其所在的VO对象:A[[scope]]: VO(G)

      image.png
    2. 执行代码let C = A(2),执行了A(2),所以会创建一个新的执行上下文EC(A),并入栈:
      1) 初始化其this指向,因为没有对象调用它,所以它指向全局对象window
      2) 初始化其作用域链,指向其作用域 :A[[scopeChain]] = VO(G)
      3) 创建AO(A)对象: 初始实参、形参赋值、执行内部代码

      image.png
    3. 执行内部代码时,会发现B的值(AAAFFF111)返回出去被C占用了,也就是C指向了AO(A)中的堆内存在,导致EC(A)不能出栈销毁,否则C就找不到它了,这就形成了闭包,闭包有两个作用:
      1) 因为B的值(AAAFFF111),其实只跟EC(A)环境有关,跟EC(G)无关,这就形成了变量保护,也就是我们说的变量私有化
      2) 保存变量,因为变量一直被占用,所以无法销毁,这就起到在ECStack中保存变量的目的

    4. A(2)执行完后,执行C(3),所以会创建一个新的执行上下文EC(C),并入栈:
      1) 初始化其this指向,因为没有对象调用它,所以它指向全局对象window
      2) 初始化其作用域链,指向其作用域,因为C实际上指向的是B,而B的作用域又是AO(A),所以 :C[[scopeChain]] = AO(A)
      3) 创建AO(C)对象: 初始实参、形参赋值、执行内部代码

      image.png
    5. 执行到内部console.log(x + y + z)时,发现x和y变量没有在AO(C)中,所以向其作用域链AO(A)查找,发现AO(A)中有x = 2 , y = 2z在自身AO(C)中有z= 3,所以结果为7
      下图中的这两条,就形成了一个完整的作用域链AO(C) => AO(A) => VO(G)

      image.png

    相关文章

      网友评论

          本文标题:javascript是怎么执行的?

          本文链接:https://www.haomeiwen.com/subject/fhltsltx.html