美文网首页
学习笔记——JavaScript执行过程

学习笔记——JavaScript执行过程

作者: SleepWalkerLj | 来源:发表于2022-01-20 14:30 被阅读0次

    第一步,解析代码

    当V8引擎开始解析JavaScript代码时,其内部会在堆内存中创建一个全局对象(GlobalObject也称为go),里面会包含String,Array,Number,setTimeOut等等一些全局对象和全局函数。这就是为什么能直接在代码中使用String、Math、Date这些类,或者使用setTimeout、setInterval这些函数的原因了。

    var num1 = 10;
    var num2 = 20;
    var result = num1 - num2;
    
    // 伪代码示意
    var globalObject = {
      String: "类",
      Date: "类",
      setTimeount: "函数",
      // 指向自己的Window属性
      window: globalObject,
      // 解析时会将这些属性定义到go中,但是此时代码还未执行,所以值都是undefined
      num1: undefined,
      num2: undefined,
      result: undefined,
    };
    

    第二步,运行代码

    1. Execution Context Stack

    v8引擎为了执行代码, 内部存在一个执行上下文栈(Execution Context Stack, 简称ECStack)(函数调用栈),是一个执行代码的调用栈。ECStack是一个栈结构,js代码执行函数时,就会把这个函数压入栈中,执行完便会将这个函数弹出栈。

    函数调用栈.png
    2. Global Execution Context
    • ECStack一般是用于执行函数的,但是上面要执行的是全局代码, 所以创建了全局执行上下文(Global Execution Context)(全局代码需要被执行时才会创建),就是构建一个全局代码块放入ECStack中执行。
    • GEC有两部分组成,一个用来维护一个VO(variable Object),它指向GO对象;另一个用来执行代码(自上往下依次执行),如图中先执行var num1=10,就会通过VO找到GO里的num1:undefined,然后将undefined改成10
    • console.log(window)打印的就是go对象,执行时会通过AO找到GO里的window属性,这个属性又是指向go自身的。
      执行GEC.png

    函数执行

    执行函数时,在函数代码的前面就可以执行函数。这是因为在解析函数时,js引擎会为函数开辟一块内存空间来存储它,这块空间会存放函数父级作用域和函数的执行体,下面的bar函数的执行体就是var b = 10; console.log("bar");,父级作用域是globalObject也就是window。

    bar()
    function bar(a) {
      var b = 10
      console.log("bar");
    }
    
    // 伪代码示意
    var globalObject = {
      String: "类",
      Date: "类",
      setTimeount: "函数",
      window: globalObject,
      // 这里保存的是bar函数的地址
      bar: 0x100
    };
    
    1. 解析时先生成了一个Go对象,发现里面有个bar函数,就会在内存开辟一块空间用于存放bar函数的父级作用域和执行体,然后把这个地址复制给bar属性。
    2. 解析完后开始执行代码,执行bar时,通过AO找到Go里的bar,发现这个bar是个内存地址,就会根据这个地址找到为bar函数创建的空间。然后继续执行(),括号是执行函数的意思,此时js引擎会创建一个函数执行上下文(Function Execution Context,简称FEC)用来存放bar函数空间里的内容,然后将FEC放入ECStack中执行。
    3. FEC中也有一个VO,指向的是AO对象(自动创建的),用于定义函数内部的参数,变量等等(未赋值),开始执行函数体时才会进行赋值等操作。执行完后这个FEC就会弹出栈并销毁,AO失去指向也会被销毁。如果后面的代码又执行了bar函数,就会重新创建FEC和AO。
    函数执行.png

    作用域链

    • 当我们查找一个变量时,真实的查找路径是沿着作用域链来查找。FEC中的scope chain:VO+ParentScope就是作用域链,由当前VO对象和父级作用域组成,这里bar函数的父级作用域就是全局对象了(在函数编译时就被确定了),就是GEC中的GO对象。函数的嵌套也是同样的道理,顺着作用域链层层向上查找。
    • 如下面的代码在bar函数里打印name,就会顺着scope chain,先从当前函数的VO开始找,此时的VO指向AO,发现AO中没有,会去找父级作用域,就是一开始的全局执行上下文(GEC),GEC的作用域就是GEC的VO,也就是GEC的GO,发现里面有abc,就会使用GEC的abc变量。
    var name = 'lj'
    bar()
    function bar(a) {
      var b = 10
      console.log(name);
    }
    
    作用域链.png

    练练手

    var message = "Hello Global"
    
    function foo() {
      console.log(message)
    }
    
    function bar() {
      var message = "Hello Bar"
      foo()
    }
    
    bar()
    

    这是一个典型的作用域链问题,最终打印的结果是“Hello Global”,因为在foo函数定义时,他的父级作用域是全局作用域,也就是GO,foo函数的作用域并不受调用位置的影响,而是和定义的位置有关系的。

    var a=10
    function fun(){
      console.log(a)
      return
      var a = 20
    }
    fun()
    

    这里会打印“undefined”,因为定义时,fun函数的VO是有a:undefined的,return只是不执行赋值的操作。

    理解

    无论是执行全局变量,还是函数,都会进入ECStack中执行,全局变量被包裹到全局执行上下文(GEC)里进入ECStack中执行,而函数会被包裹到函数执行上下文(FEC)里进入ECStack中执行。这两个上下文都会在内部创建一个VO对象,分别用于指向GEC的GO和FEC的AO,这里的AO是执行函数时创建的对象,用于存放函数内的参数、变量等属性。执行代码时,用到的变量会按照作用域链进行查找,先从本身的AO或GO中寻找,如果没有则会去父级作用域中查找,就是父级的AO或者GO。

    文章的内容都是基于早期的ECMA的版本规范,就是es5之前的规范。es5之后的版本,将VO改成了VE(Variable Environment 变量环境),在执行代码中变量和函数的声明会作为环境记录添加到变量环境VE中,但形式不严格要求成对象,可以是map、list或者对象等等,基本的逻辑是差不多的。

    相关文章

      网友评论

          本文标题:学习笔记——JavaScript执行过程

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