理解 JavaScript 中的执行上下文

一块儿看看 JavaScript 程序内部是如何执行的。javascript

本文翻译自 blog.bitsrc.io/understandi…,做者 Sukhjinder Arora,有部分删改。前端

若是你想成为一个合格的 JavaScript 开发者,你必须知道它的内部是如何执行的。掌握 JavaScript 执行上下文和执行栈对理解变量提高、做用域和闭包很是重要。java

理解执行上下文和执行栈将使你成为一个更加优秀的 JavaScript 开发者。编程

执行上下文是什么?

执行上下文是一个 JavaScript 代码运行的环境。任何 JavaScript 代码执行的时候都是处于一个执行上下文中。windows

执行上下文的类型

JavaScript 中一共有三种执行上下文。数组

  • 全局执行上下文(Global) -- 它是默认的基本执行上下文。代码要么在全局执行上下文要么在函数执行上下文。它有两个特征:它会建立一个全局对象(在浏览器中就是 window)而且会把 this 设置为全局对象 windows。在一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 -- 当函数执行的时候,一个新的函数执行上下文就会建立。每一个函数都有本身的执行上下文,当函数执行的时候上下文会被建立。函数执行上下文能够建立任意多个,每一个执行上下文被建立的时候会经历若干步骤,接下来将会讨论。
  • eval 函数执行上下文 -- 在 eval 函数中执行的代码也会有本身的自行上下文,但因为 eval 已经不经常使用了,因此不作讨论。

执行栈

执行栈(执行上下文栈),在其余编程语言中也叫调用栈,是一个后进先出的结构。它用来存储代码执行过程当中建立的全部执行上下文。浏览器

当 JavaScript 引擎执行你的代码时,它会建立一个全局执行上下文而且将它推入当前的执行栈。当执行碰到函数调用的时候,它会为这个函数建立执行上下文并把这个执行上下文推入执行栈顶部。闭包

引擎执行处于栈顶的上下文对应的函数。当函数执行完毕,它的上下文就会从栈顶弹出,引擎接着继续执行新处于顶部的上下文对应的函数。编程语言

看看下面的例子:ide

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
复制代码

上面代码的执行上下文栈

上面代码在浏览器中执行时,JavaScript 引擎会先建立一个全局执行上下文并把它推出执行栈中。碰到 first() 执行时,引擎给这个函数建立一个新的执行上下文,而后把它推入执行栈顶部。

second()first() 函数内部执行时,引擎会给 second 建立上下文并把它推入执行栈顶,当 second 函数执行完毕,它的执行上下文就会从执行栈顶弹出,指针会指向它下面的上下文,也就是 first 函数的上下文。

first 函数执行完毕,它的执行栈也会从栈顶弹出,指针就指向了全局执行上下文。当全部的代码执行完毕,引擎会把全局执行上下文也从执行栈中移出。

执行上下文是如何建立的

从上面的过程,咱们已经了解了 JavaScript 引擎是如何管理执行上下文的,接下来咱们看看引擎是如何建立执行上下文的。

执行上下文会经历两个阶段:1 建立阶段;2 执行阶段。

建立阶段

执行上下文在建立阶段就会被建立。建立阶段作下面两件事:

  1. 建立词法环境(LexicalEnvironment)
  2. 建立变量环境(VariableEnvironment)

因此从概念上说,执行上下文能够用下面的方式表示:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}
复制代码

词法环境(Lexical Environment)

ES6 官方文档是这样定义词法环境的

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

简单来讲,词法环境是一种表示标识符和变量的映射关系的环境。在词法环境中,标识符指向变量或者函数,变量是指对象(包括函数对象和数组对象)或者原始值。

举个例子,看看下面的代码

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}
复制代码

上面代码的词法环境以下

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}
复制代码

每一个词法环境由三个部分组成:

  1. 词法环境内部的环境记录(Environment Record);
  2. 一个指向外层词法环境的可空引用(Reference to the outer environment);
  3. this
环境记录(Environment Record)

Environment Record 是在词法环境中存储变量和函数的地方。

Environment Record 有下面两种:

  • Declarative environment record -- As its name suggests stores variable and function declarations. The lexical environment for function code contains a declarative environment record.
  • Object environment record -- The lexical environment for global code contains a objective environment record. Apart from variable and function declarations, the object environment record also stores a global binding object (window object in browsers). So for each of binding object’s property (in case of browsers, it contains properties and methods provided by browser to the window object), a new entry is created in the record.

上面是原文,简单解释下:

  • Declarative environment record -- 用来放变量或者函数声明,函数中的词法环境都是这种;
  • Object environment record -- 指向全局对象 window(在浏览器中),全局词法环境是这种;

注意:对于函数,环境记录也包括一个 arguments 对象。arguments 是一个类数组对象,它包含索引和参数值的映射。看看下面的例子:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
复制代码
outer 是什么

outer 表示一个做用域指向的外层词法环境。在查找变量时,若是在当前的词法环境里面没有找到变量,那就经过 outer 找到外层的词法环境,而后再在外层的词法环境里面查找变量,若是尚未找到,则会继续往外层找,一直找到全局做用域。

this 怎么肯定

在全局执行上下文中,this 指向全局对象 window(在浏览器中)。

在函数执行上下文中,this 取决于函数是如何被调用的。这是咱们常常弄混的一点。若是是经过对象调用的函数,那 this 指向这个对象。不然 this 将会指向全局对象(在浏览器中是 window)或者 undefined(严格模式下) 。 看下面的例子:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge();
// 'this' 指向 'person', 由于 'calcAge' 是经过 `person` 对象调用的。
const calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局对象,由于函数不是经过对象引用的方式调用的。
复制代码

词法做用域用伪代码表示是这样的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}
复制代码

变量环境(Variable Environment)

变量环境也是一个词法环境,它和词法环境长得同样。区别在于,在 ES6 中,词法环境用来存储函数声明和 letconst 声明的变量,变量环境仅仅用来存储 var 声明的变量。

执行阶段

在执行阶段会完成变量的赋值,代码会被执行。

看下面的例子:

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

当上面的代码执行的时候,JavaScript 引擎会建立一个全局执行上下文来执行全局的代码。因此在建立阶段(creation phase)全局执行上下文是像这样的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}
复制代码

在执行阶段(execution phase),会进行变量赋值。全局执行上下文将会变成下面这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}
复制代码

当碰到要执行 multiply(20, 30) 时,一个新的函数执行上下文会建立。在建立阶段(creation phase)函数执行上下文会像下面这样:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2}, // 函数的参数也在词法环境中
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}
复制代码

在执行阶段(execution phase)会进行变量赋值。赋值以后的函数执行上下文以下:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}
复制代码

函数执行完成时,返回的值将会赋值给 c,全局词法环境将会更新,而后全部代码执行完毕,程序结束。

你可能注意到了 letconst 声明的变量在建立阶段(creation phase) 和它的值没有任何关联,可是 var 声明的变量被赋予了 undefined

这是由于在建立阶段 JavaScript 引擎会扫描到变量和函数声明。用 var 声明的变量被初始化为 undefined,用 let const 声明的变量将不会被初始化。后者将会造成暂时性死区,提早使用它们将会报错。

暂时性死区

这就是变量提高。

注意,在执行阶段,若是引擎发现 let 声明的变量并无被赋值,引擎将会把它赋值为 undefined

最后

感谢阅读,欢迎关注个人公众号 云影 sky,带你解读前端技术。。

公众号
相关文章
相关标签/搜索