理解 JavaScript 中的执行上下文和执行栈

914 阅读8分钟

执行上下文

概念

js代码执行的环境,每当js代码执行的时候,都是在执行上下文中进行的。

分类

  1. 全局执行上下文:这是默认的执行上下文,所有不在函数内的代码都在全局执行上下文中。全局执行上下文会做两件事:(1)创建一个window对象(在浏览器中)。(2)将this指向window对象。注意,一个程序中只有一个全局执行上下文。
  2. 函数执行上下文:每一个函数被调用的时候就会创建一个函数执行上下文。函数执行上下文可以有很多个。
  3. Eval函数执行上下文:eval函数可以计算某个字符串,并执行其中的js代码,这样就会存在一个安全性问题,在代码字符串未知或者是来自于用户输入源的话,绝对不要使用eval函数。

举例

// 全局执行上下文
var sayHello = 'Hello';    
function person(){ // 函数执行上下文
    var firstName = 'xiao';
    var lastName = 'ming';
    function getFirst() { // 函数执行上下文
        return firstName;
    }
    function getLast() {//函数执行上下文
        return lastName;
    }
    return sayHello + getFirst() + getLast(); // 创建函数执行上下文
}
person(); // 创建函数执行上下文

`

执行上下文生命周期

  1. 创建阶段:在创建阶段会做以下三件事: this绑定、创建词法环境、创建变量环境。可如下表示

     ExecutionContext = {
         ThisBinding = <this value>,//绑定this
         LexicalEnvironment = { ... },// 创建词法环境
         VariableEnvironment = { ... },//创建变量环境
     }
    

    (1)this绑定,在全局执行上下文中this指向window对象。在函数执行上下文中,this的指向取决于函数如何被调用。如果函数是在全局中调用那么this指向window或者undefined(在严格模式下),如果函数是被其他对象调用,那么this指向该对象。

     var word = 'hello window';
     var obj = {
         word: 'hello',
         sayHello: function() {
             console.log(this.word);
         }
     };
     obj.sayHello() // sayHello函数被obj调用那么this指向obj,所以打印‘hello'。
     var temp = obj.sayHello; //将sayHello的指针复制给temp。
     temp(); // 全局调用sayHello函数,那么this指向window,打印‘hello window';
    

    (2)创建词法环境,词法环境是用来登记变量声明、函数声明、函数声明的形参的地方。后续代码执行的时候就知道去哪里取变量的值和函数了。词法环境由两部分组成:环境记录、对外部词法环境的引用。

    环境记录:用来存储变量和函数声明的地方。

    (1)声明式环境记录:声明式环境记录也比较特殊,它只记录非var声明的标识符,例如let、const、function……声明的标识符等等。并且它没有关联的绑定对象。所有声明的标识符(这里应该包含var声明的标识符,但不建立关联)都位于此处。将所有非var声明的标识符实例化,但不初始化,也就是变量处于uninitialized状态。也就是说内存中已经为变量预留出空间,但是还没有和对应的标识符建立绑定关系。在执行上下文的运行(perform状态)阶段,并执行到声明语句时,才会真正初始化并默认赋值为undefined。所以你就懂了,let声明的标识符之前无法访问,就是因为还没有建立绑定。暂存死区的根本原因在此。

    (2)对象式环境记录:对象式记录也是用于记录标识符与变量的映射,但是它只记录var声明的标识符 ; 并且它有一个关联的绑定对象(binding object)。在词法环境中,会为对象式环境记录中所有的标识符绑定到绑定对象的同名属性上。例如var number=1000;也能够通过window.number形式获取到number的值。

    简而言之,在全局环境中,环境记录器是对象环境记录器。在函数环境中,环境记录器是声明式环境记录器。

    对外部词法环境的引用:有了这个引用,就可以访问父级词法环境中存储的变量和函数。它就是作用域链能串联的关键。

(3)创建变量环境,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量和函数声明。

举例:

let a = 1;
const b = 2;
var c = 3;
function test (d, e) {
    var f = 10;
    return f * d * e;
}
c = test(a, b);

以上代码创建的执行上下文如下:

GlobalExectionContext = {
  // 全局执行上下文
  ThisBinding: <Global Object>,//绑定this
  LexicalEnvironment: {// 词法环境,let和const声明的变量放在这里
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
    }
    outer: <null>
  },
  VariableEnvironment: {//变量环境,var声明的变量和函数声明放在这里
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
      test: <func>,
    }
    outer: <null>
  }
}
FunctionExectionContext = {
  // 函数执行上下文,注意只有遇到调用函数 test 时,函数执行上下文才会被创建
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符,参数
      Arguments: {0: 1, 1: 2, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      f: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

可能你已经注意到 let 和 const 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined。这是因为在创建阶段时,引擎检查代码找出var声明的变量和函数声明。进行变量声明提升,而let和const声明的变量不会被提升,这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。

  1. 执行阶段

代码开始在创建阶段形成的执行上下文中运行的阶段,并逐行分配变量值。在执行开始时,JS 引擎在其创建阶段对象中寻找执行函数的引用。如果不能在自己的作用域内找到它,它将继续向上查找,直到到达全局环境。

如果在全局环境中没有找到引用,它将返回一个错误。但是,如果找到了一个引用,并且函数执行正确,那么这个特定函数的执行上下文将从堆栈中弹出,JS 引擎将移动到下一个函数,在那里,它们的执行上下文将被添加到堆栈中并执行,依此类推。

咱们通过示例查看上面的两个阶段,以便更好地理解它。 在创建阶段,全局执行上下文类似于这样

GlobalExectionContext = {
  // 全局执行上下文
  ThisBinding: <Global Object>,//绑定this
  LexicalEnvironment: {// 词法环境,let和const声明的变量放在这里
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
    }
    outer: <null>
  },
  VariableEnvironment: {//变量环境,var声明的变量和函数声明放在这里
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
      test: <func>,
    }
    outer: <null>
  }
}

let (a)和const (b)定义的变量在创建阶段没有任何关联的值,但是var (c)定义的变量会被设置为undefined。但是在声明let和 const变量之前访问它们时,会得到一个引用错误。这就是咱们所说的变量提升,即所有使用var的变量声明都被提升它们的局部作用域(在函数内部声明)或者全局作用域的顶部(在函数外部声明的)。在执行阶段,完成变量分配。所以全局执行上下文在执行阶段类似如下:

GlobalExectionContext = {
  // 全局执行上下文
  ThisBinding: <Global Object>,//绑定this
  LexicalEnvironment: {// 词法环境,let和const声明的变量放在这里
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: 1,
      b: 2,
    }
    outer: <null>
  },
  VariableEnvironment: {//变量环境,var声明的变量和函数声明放在这里
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: 3,
      test: <func>,
    }
    outer: <null>
  }
}

3. 回收阶段

执行上下文出栈等待虚拟机回收执行上下文。

执行栈

通常,我们的代码中都不止一个上下文,那这些上下文的执行顺序应该是怎样的?栈,是一种数据构,具有先进后出的原则。JS中的执行栈就具有这样的结构,当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入执行栈,每遇到一个函数调用,就会往栈中压入一个新的上下文。引擎执行栈顶的函数,执行完毕,弹出当前执行上下文。看以下例子:

var sayHello = 'Hello';    
function person(){ // 函数执行上下文
    console.log(1);
    var firstName = 'xiao';
    var lastName = 'ming';
    function getFirst() { // 函数执行上下文
        console.log(2);
        return firstName;
    }
    function getLast() {//函数执行上下文
        console.log(3)
        return lastName;
    }
    let first = getFirst();
    let second = getLast();
    return sayHello + first + second; // 创建函数执行上下文
}
person(); // 创建函数执行上下文

以引例来说明。当person() 函数被调用,将person 函数的执行上下文压入执行栈,接着执行输出 ‘1’;当getFirst() 函数被调用,将getFirst 函数的执行上下文压入执行栈,接着执行输出 ‘2’;将getFirst()执行完毕,被弹出执行栈,person() 函数接着执行,getLast()函数被调用,输出 ‘3’;person() 函数接着执行,执行完毕被弹出执行栈。