JavaScript学习-理解JavaScript中的执行上下文和执行栈
一、什么是执行上下栈?
简而言之,执行上下文就是评估和执行 JavaScript 代码的环境的抽象概念。每当 JavaScript 代码在运行的时候,它都是在执行上下文中运行的。
二、执行上下文的类型?
JavaScript 中有三种执行上下文类型。
(1)全局执行上下文:这是默认的或者说是基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象,一个程序中只会有一个全局执行上下文。
(2)函数执行上下文:每当一个函数被调用的时候,都会为该函数创建一个新的上下文,每个函数都有它自己的执行上下文,不过是在函数被调用的时候创建的,函数上下文可以有多个,每当一个新的执行上下文被创建的时候,它会按定义的顺序执行一系列的步骤。
(3)Eval函数执行上下文:执行在eval 函数内部的代码也会有他属于自己的执行上下文,但是由于 JavaScript 开发者并不经常使用 eval ,这里不会讨论它。
三、执行栈
执行栈,也就是其他编程语言中所说的“调用栈”,是一种拥有LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本的时候,它会创建一个全局的执行上下文并且压入当前的执行栈中。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数,当该函数执行结束的时候,执行上下文从栈中弹出,控制流程到达当前栈的下一个上下文。
我们可以通过下面的代码实例来理解:
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');
输出结果:
Inside first function
Inside second function
Again inside first function
Inside Global Execution Context
上述代码的执行上下文栈。
四、怎么创建执行上下文?
到现在,我们已经理解了 JavaScript 怎么管理执行上下文了,现在让我们来了解 JavaScript 引擎是怎么创建执行上下文的。
创建执行上下文有两个阶段:1.创建阶段 ,2、执行阶段。
The Creation Phase
在 JavaScript 代码执行前,执行上下文将经历创建阶段,在创建阶段会发生三件事:
1、this 值的决定,即我们所熟悉的 This 绑定。
2、创建词法环境组件。
3、创建变量环境组件。
所以执行上下文在概念上表示如下:
ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, }
This绑定
在全局执行上下文中,this 的值的指向全局对象。(在浏览器中,this 引用的是window对象)
在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它是被一个引用对象调用,那么 this 会被设置为那个对象,否则 this 的值被设置为全局对象或者是 undefined(在严格模式下)。例如:
let foo = { baz: function() { console.log(this); } } foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被 // 对象 'foo' 调用 let bar = foo.baz; bar(); // 'this' 指向全局 window 对象,因为 // 没有指定引用对象
词法环境
官方的ES6 文档把词法环境定义为:
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。
一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。
简单的来说,词法环境是一种持有 标识符-变量映射 的结构。(这里的 标识符 指的是变量/函数的名字,而 变量 是对实际对象[包含函数类型对象]或者原始数据的引用)。
现在,在词法环境的内部有两个组件:(1) 环境记录器和(2)一个外部环境的引用。
1、环境记录器 是存储 变量 和 函数声明 的实际位置。
2、外部环境的引用 意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:
1、全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null,它拥有内建的Object/Array 等,在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有用户自定义的全局变量,并且 this 的值指向全局对象,并且 this 的值指向全局对象。
2、在函数环境中,函数内部用户定义的变量存储在环境记录器中,并且引用的外部环境可能是全局环境,或者是任何包含此内部函数的外部函数。
环境记录器也有两种类型,
1、声明式环境记录器 存储变量,函数和参数。
2、对象环境记录器 用来定义出现在全局上下文中的变量和函数的关系。
简而言之,
1、在全局环境中,环境记录器就是对象环境记录器。
2、在函数环境中,环境记录器就是声明式环境记录器。
注意-对于函数环境,声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的length。
抽象的讲,词法环境在伪代码中看起来是这样:
// 全局环境 GlobalExectionContext = { // 词法环境(环境记录器和外部环境的引用) LexicalEnvironment: { // 环境记录器 EnvironmentRecord: { Type: "Object", // 在这里绑定标识符 } // 外部环境的引用 outer: <null> } } 函数环境 FunctionExectionContext = { // 词法环境 LexicalEnvironment: { // 环境记录器 EnvironmentRecord: { Type: "Declarative", // 在这里绑定标识符 } // 外部环境的引用 outer: <Global or outer function environment reference> } }
变量环境
它同样是一个词法环境,其环境记录器持有 变量声明语句 在执行上下文中创建的绑定的关系。
如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。
在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);
执行上下文看起来是下面这样的:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 在这里绑定标识符 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 在这里绑定标识符 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 在这里绑定标识符 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 在这里绑定标识符 g: undefined }, outer: <GlobalLexicalEnvironment> } }
注意 只有遇到调用 multiply 时,函数执行上下文才会被创建。
可能你已经注意到 let 和 const 定义的变量并没有任何关联,但是 var 定义的变量被设定了 undefined 。
这是因为在创建阶段,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined (var 声明的情况下),或者未初始化(let 和 const 的情况下)。
这就是你可以在声明之前访问 var 声明的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。
这就是我们说的变量声明提升。
执行阶段
在此阶段,完成对所有这些变量的分配,最后执行代码、
注意 , 在执行阶段,如果是 JavaScript 引擎不能在源码声明的实际位置找到 let 变量的值,它会被赋值为 undefined。
五、结论
我们已经讨论过 JavaScript 程序内部是如何执行的。虽然要成为一名卓越的 JavaScript 开发者并不需要学会全部这些概念,但是如果对上面概念能有不错的理解将有助于你更轻松,更深入地理解其他概念,如变量声明提升,作用域和闭包。
作者:子非
链接:https://juejin.cn/post/6844903682283143181
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。