深入 JavaScript 的执行上下文
JavaScript中的执行上下文指的就是JavaScript的执行环境。根据 ECMA-262的第六版规范定义:
An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation
JavaScript执行上下文可以理解为编译和执行JavaScript代码时所处的抽象环境概念。JavaScript的代码执行都是在执行上下文中进行的。ECMA-262 不同版本对JavaScript执行上下文的描述不同,在 ES5之前,执行环境有变量对象(VO)、活动对象(AO)的描述;而在ES5中引入了词法环境和变量环境的概念,并定义了三种执行上下文,分别是:全局执行上下文、函数执行上下文、Eval 函数执行上下文。而ES6及之后的版本中,对词法环境中的环境记录进行了更明确的细分。
一、执行环境与作用域
最早接触执行环境与作用域是在JavaScript高级程序设计中的概念。总结一下就是:
1、执行环境是一个隔离的环境,定义了环境中代码有权访问的数据。
程序设计中,代码对变量的访问权限控制很重要。通过最小特权原则,严格限制变量的访问权限,js中是通过执行环境来实现的。
2、环境中的有一个关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
类似于在全局环境中使用 var声明一个变量,该变量会自动变成全局对象window的属性。在函数的局部环境中,声明的变量和函数同一样会挂载到一个对象上,成为对象的属性,该变量即变量对象。但与全局对象不同的是,变量对象无法通过代码访问。
3、代码在环境中执行时,会创建变量对象的作用域链(scope chain)属性,用于保证对变量和函数的有序访问。
在当前执行环境中找不到对应的标识符,将会搜索变量对象的作用域链。作用域链不仅保存了当前环境的变量对象,还拥有外部环境的变量对象引用,因此可以“由内而外”的访问访问变量。
4、活动对象和变量对象实际上是一个对象,只是在JavaScript中编译和执行阶段不同的叫法。参考
JavaScript中的编译概念使用即时编译或者预编译表达更合适。
5、实际上,执行环境中还有 this 对象的概念上述未提及,但在第七章节做了补充。原文是this对象是在运行时基于函数的执行环境绑定的。
重要的概念是 this对象是运行时确定的,而非定义时。理解了这段就明白JavaScript中的this为何如此变化莫测了。
二、 可执行代码与执行上下文( ECMA-262 第五版)
ES5规范定义了执行上下文(执行环境)中的三种构成。分别是,词法环境、可变环境 和this绑定。
1、词法环境
ECMA-262 第五版中对词法环境的定义:
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
翻译过来就是,词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由环境记录和对外部词法环境的可能为 null 的引用组成。
通俗来说,词法环境实际上就是对某个环境中的变量和函数声明的记录。类似于上述变量对象的概念。
环境记录是变量和函数声明存储在词法环境中的地方,有两种类型的环境记录,分别是,声明式环境记录和对象式环境记录。声明式环境记录顾名思义会储存变量声明和函数声明,将标识符与对应的值相关联。而对象式环境记录会将标识符与某些对象的属性相关联。具体讨论一下两者之间的区别与联系。
声明式环境记录
通常情况下,声明式环境记录绑定的内容会被直接存储在底层实现上,如虚拟机的寄存器上,以便于快速访问,这也是与老版ES3中的激活对象主要区别。另外声明式环境记录项的特性允许使用完整的词法寻址技术,无需任何作用域链查找即可直接访问所需的变量。
声明式环境记录除了支持可变绑定之外,还提供不可变绑定。不可变绑定是一种标识符和值之间的关联一旦建立就不能修改的绑定。如果绑定的内容是不可更改的,所有变量的地址在编译时就可以确定,这样js引擎在执行时就可以针对性进行优化。
使用声明性环境记录替换旧的激活对象最重要的原因就是执行效率。JavaScript语言的作者也提到:
—ES3 中的激活对象实现只是“一个bug”:“我注意到 ES5 中有一些真正的改进,特别是第 10 章现在使用声明式绑定环境. ES1-3 滥用对象作为作用域(在 1995 年的 JS 中,我又一次这样做了,因为它在快速实现语言所需的对象上节省大量的时间)是一个错误,而不是一个特性”。
使用伪代码表示声明式环境记录:
environment = { // storage environmentRecord: { type: "declarative", // storage }, // reference to the parent environment outer: <...> };
eval 函数会破坏V8的优化,以下代码运行在Chrome 93 环境。
V8对于函数的执行进行了优化,没有创建arguments对象,也没有捕获父函数中的变量。这样的函数是轻量级的。加入了eval函数之后,环境记录中增加了arguments对象和closure闭包,即父环境。eval函数的存在破坏了V8的内部的优化,因为函数内部有内部函数,因此很难分析内部函数是否引用 arguments。
对象式环境记录
对象环境记录用于定义出现在全局上下文和with
语句内部的变量和函数的关联。每次with
执行语句时,都会创建一个带有对象环境记录的新词法环境。运行上下文的环境被这个新创建的环境替换。with
执行完毕上下文环境恢复到以前的状态。
var a = 10; var b = 20; with ({a: 30}) { console.log(a + b); // 50 } console.log(a + b); // 30, restored
使用伪代码表示:
// initial state context.lexicalEnvironment = { environmentRecord: {a: 10, b: 20}, outer: null }; // "with" executed previousEnvironment = context.lexicalEnvironment; withEnvironment = { environmentRecord: {a: 30}, outer: context.lexicalEnvironment }; // replace current environment context.lexicalEnvironment = withEnvironment; // "with" completed, restore the environment back context.lexicalEnvironment = previousEnvironment;
由于对象环境记录效率低下,with
语句已经从ES5严格模式中删除。
词法环境中另一个重要的核心是外部环境引用。对外部环境的引用意味着它可以访问其外部词法环境。如果在当前词法环境中找不到变量,JavaScript 引擎可以在外部环境中查找变量,类似于作用域链的概念。
2、可变环境
可变环境也是一个词法环境,具有上面定义的词法环境的所有属性和组件。词法环境与可变环境的区别在于,前者用于存储函数声明和变量(let
和const
)绑定,而后者仅用于存储变量(var)
绑定。
3、this绑定
相关文章诸多,不再赘述了。this是在运行时绑定的,有默认绑定、隐式绑定、显示绑定。
参考链接:
https://262.ecma-international.org/5.1/#sec-10.2
http://dmitrysoshnikov.com/
https://262.ecma-international.org/7.0/#
https://blog.csdn.net/szengtal/article/details/78726178
http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-2-lexical-environments-ecmascript-implementation/#structure-of-execution-context
https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0#