zoukankan      html  css  js  c++  java
  • 什么是JS执行上下文?

    我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如:

    function f1() {
        console.log('听风是风');
    };
    f1(); //echo
    
    function f1() {
        console.log('echo');
    };
    f1(); //echo

    按照代码书写顺序,应该先输出 听风是风,再输出 echo才对,很遗憾,两次输出均为 echo;如果我们将上述代码中的函数声明改为函数表达式,结果又不太一样:

    var f1 = function () {
        console.log('听风是风');
    };
    f1(); //听风是风
    
    var f1 = function() {
        console.log('echo');
    };
    f1(); //echo

    这说明代码在执行前一定发生了某些微妙的变化,JS引擎究竟做了什么呢?这就不得不提JS执行上下文的了。

    JS执行上下文

    JS代码在执行前,JS引擎总要做一番准备工作,这份工作其实就是创建对应的执行上下文;

    执行上下文有且只有三类,全局执行上下文,函数上下文,与eval上下文;由于eval一般不会使用,这里不做讨论。

    1.全局执行上下文

    全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是我们熟知的window对象,我们能通过this直接访问到它。

     全局对象window上预定义了大量的方法和属性,我们在全局环境的任意处都能直接访问这些属性方法,同时window对象还是var声明的全局变量的载体。我们通过var创建的全局对象,都可以通过window直接访问。

     2.函数执行上下文

    函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文;需要注意的是,同一个函数被多次调用,都会创建一个新的上下文

    说到这你是否会想,上下文种类不同,而且创建的数量还这么多,它们之间的关系是怎么样的,又是谁来管理这些上下文呢,这就不得不说说执行上下文栈了

    执行上下文栈(执行栈)

    执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out 先进后出)的特性。

    JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,

    所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文

    function f1() {
        f2();
        console.log(1);
    };
    
    function f2() {
        f3();
        console.log(2);
    };
    
    function f3() {
        console.log(3);
    };
    
    f1();//3 2 1

    我们通过执行栈与上下文的关系来解释上述代码的执行过程,为了方便理解,我们假象执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,因此过程大致如下:

    //代码执行前创建全局执行上下文
    ECStack = [globalContext];
    // f1调用
    ECStack.push('f1 functionContext');
    // f1又调用了f2,f2执行完毕之前无法console 1
    ECStack.push('f2 functionContext');
    // f2又调用了f3,f3执行完毕之前无法console 2
    ECStack.push('f3 functionContext');
    // f3执行完毕,输出3并出栈
    ECStack.pop();
    // f2执行完毕,输出2并出栈
    ECStack.pop();
    // f1执行完毕,输出1并出栈
    ECStack.pop();
    // 此时执行栈中只剩下一个全局执行上下文

    那么到这里,我们解释了执行栈与执行上下文的存储规则;还记得我在前文提到代码执行前JS引擎会做准备创建执行上下文吗,具体怎么创建呢,我们接着说。

    执行上下文创建阶段

    执行上下文创建分为创建阶段与执行阶段两个阶段,较为难理解应该是创建阶段,我们先说创建阶段。

    JS执行上下文的创建阶段主要负责三件事:

    1. 确定this

    2. 创建词法环境(LexicalEnvironment)

    3. 创建变量环境(VariableEnvironment)

    这里我就直接借鉴了他人翻译资料的伪代码,来表示这个创建过程:

    ExecutionContext = {  
        // 确定this的值
        ThisBinding = <this value>,
        // 创建词法环境
        LexicalEnvironment = {},
        // 创建变量环境
        VariableEnvironment = {},
    };


    如果你有阅读其它关于执行上下文的文章读到这里一定有疑问,执行上下文创建过程不是应该解释this,作用域与变量对象/活动对象才对吗,怎么跟别的地方说的不一样,这点我后面解释。

    1.确定this

    官方的称呼为This Binding,在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。

    而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象。否则this一般指向全局对象window或者undefined(严格模式)。

    2.词法环境

    词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用。(表示看不懂)

    词法环境分为全局词法环境函数词法环境两种

    全局词法环境:

    对外部环境的引入记录为null,因为它本身就是最外层环境,除此之外它还包含了全局对象的所有属性方法,以及用户自定义的全局对象(通过var声明)。

    函数词法环境:

    包含了用户在函数中定义的所有变量外,还包含了一个arguments对象。函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来。

    // 全局环境
    GlobalExectionContext = {
        // 全局词法环境
        LexicalEnvironment: {
            // 环境记录
            EnvironmentRecord: {
                Type: "Object", //类型为对象环境记录
                // 标识符绑定在这里 
            },
            outer: < null >
        }
    };
    // 函数环境
    FunctionExectionContext = {
        // 函数词法环境
        LexicalEnvironment: {
            // 环境纪录
            EnvironmentRecord: {
                Type: "Declarative", //类型为声明性环境记录
                // 标识符绑定在这里 
            },
            outer: < Global or outerfunction environment reference >
        }
    };

    3.变量环境

    变量环境可以说也是词法环境,它具备词法环境所有属性,一样有环境记录与外部环境引入。在ES6中唯一的区别在于词法环境用于存储函数声明与let const声明的变量,而变量环境仅仅存储var声明的变量。

    我们通过一串伪代码来理解它们:

    let a = 20;  
    const b = 30;  
    var c;
    
    function multiply(e, f) {  
     var g = 20;  
     return e * f * g;  
    }
    
    c = multiply(20, 30);

    我们用伪代码来描述上述代码中执行上下文的创建过程:

    let a = 20;  
    const b = 30;  
    var c;
    
    function multiply(e, f) {  
     var g = 20;  
     return e * f * g;  
    }
    
    c = multiply(20, 30);

    我们用伪代码来描述上述代码中执行上下文的创建过程:

    //全局执行上下文
    GlobalExectionContext = {
        // this绑定为全局对象
        ThisBinding: <Global Object>,
        // 词法环境
        LexicalEnvironment: {  
            //环境记录
          EnvironmentRecord: {  
            Type: "Object",  // 对象环境记录
            // 标识符绑定在这里 let const创建的变量a b在这
            a: < uninitialized >,  
            b: < uninitialized >,  
            multiply: < func >  
          }
          // 全局环境外部环境引入为null
          outer: <null>  
        },
      
        VariableEnvironment: {  
          EnvironmentRecord: {  
            Type: "Object",  // 对象环境记录
            // 标识符绑定在这里  var创建的c在这
            c: undefined,  
          }
          // 全局环境外部环境引入为null
          outer: <null>  
        }  
      }
      // 函数执行上下文
      FunctionExectionContext = {
         //由于函数是默认调用 this绑定同样是全局对象
        ThisBinding: <Global Object>,
        // 词法环境
        LexicalEnvironment: {  
          EnvironmentRecord: {  
            Type: "Declarative",  // 声明性环境记录
            // 标识符绑定在这里  arguments对象在这
            Arguments: {0: 20, 1: 30, length: 2},  
          },  
          // 外部环境引入记录为</Global>
          outer: <GlobalEnvironment>  
        },
      
        VariableEnvironment: {  
          EnvironmentRecord: {  
            Type: "Declarative",  // 声明性环境记录
            // 标识符绑定在这里  var创建的g在这
            g: undefined  
          },  
          // 外部环境引入记录为</Global>
          outer: <GlobalEnvironment>  
        }  
      }

    不知道你有没有发现,在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let  const被设置为未初始化。

    现在你总知道变量提升与函数声明提前是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。

    上下文除了创建阶段外,还有执行阶段,这点大家应该好理解,代码执行时根据之前的环境记录对应赋值,比如早期var在创建阶段为undefined,如果有值就对应赋值,像let const值为未初始化,如果有值就赋值,无值则赋予undefined。

    总结

    1.全局执行上下文一般由浏览器创建,代码执行时就会创建;函数执行上下文只有函数被调用时才会创建,调用多少次函数就会创建多少上下文。

    2.调用栈用于存放所有执行上下文,满足FILO规则。

    3.执行上下文创建阶段分为绑定this,创建词法环境,变量环境三步,两者区别在于词法环境存放函数声明与const let声明的变量,而变量环境只存储var声明的变量。

    4.词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不一样,全局为null,函数为全局环境或者其它函数环境。环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。

    5.你应该明白了为什么会存在变量提升,函数提升,而let const没有。

    6.ES3之前的变量对象与活动对象的概念在ES5之后由词法环境,变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。

  • 相关阅读:
    Google开源单元測试框架Google Test:VS2012 配置
    ubuntu16.04 uninstall cuda 9.0 completely and install 8.0 instead
    ubuntu 16.04 安装cuda的方法
    ubuntu垃圾文件清理方法
    行人检测资源(下)代码数据
    行人检测资源(上)综述文献
    开源深度学习架构Caffe
    python pip 安装库文件报错:pip install ImportError: No module named _internal
    Canny算子
    vmware中nat模式中使用静态ip后无法上网的问题
  • 原文地址:https://www.cnblogs.com/Rivend/p/12616528.html
Copyright © 2011-2022 走看看