学习到javascript的运行机制时,有几个概念常常出如今各类文章中且容易混淆。Execution Context(执行环境或执行上下文),Context Stack (执行栈),Variable Object(VO: 变量对象),Active Object(AO: 活动对象),LexicalEnvironment(词法环境),VariableEnvironment(变量环境)等,特别是 VO,AO以及LexicalEnvironment,VariableEnvironment的区别不少文章都没有涉及到。所以我查看了一些国内外的文章,结合自身理解写下了下面的笔记。虽然由于自身不足致使理解上的误差,可是依然相信读完下文会对理解javascript的一些概念如变量提高,做用域和闭包有很大的帮助。javascript
了解javascript的运行机制,首先必须掌握两个基本的概念。Execution Context(执行环境或执行上下文)和Context Stack (执行栈)java
咱们知道javascript是单线程语言,也就是同一时间只能执行一个任务。当javascript解释器初始化代码后,默认会进入全局的执行环境,以后每调用一个函数,javascript解释器会建立一个新的执行环境。git
var a = 1; // 1.初始化默认进入全局执行环境
function b() { // 3.进入b 的执行环境
function c() { // 5. 进入c的执行环境
···
}
c() // 4.在b的执行环境里调用c, 建立c的执行环境
}
b() // 2. 调用b 建立 b 的执行环境
复制代码
执行环境的分类:es6
从一个简单的例子开始讲起github
function foo(i) {
if (i < 0) return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(2);
复制代码
如何存储代码运行时的执行环境(全局执行环境,函数执行环境)呢,答案是执行栈。而栈遵循的是先进后出的原理,javascript初始化完代码后,首先会建立全局执行环境并推入当前的执行栈,当调用一个函数时,javascript引擎会建立新的执行环境并推到当前执行栈的顶端,在新的执行环境中,若是继续发生一个新函数调用时,则继续建立新的执行环境并推到当前执行栈的顶端,直到再无新函数调用。最上方的函数执行完成后,它的执行环境便从当前栈中弹出,并将控制权移交到当前执行栈的下一个执行环境,直到全局执行环境。当程序或浏览器关闭时,全局环境也将退出并销毁。segmentfault
所以输出的结果为:浏览器
begin:2
begin:1
begin:0
end:0
end:1
end:2
复制代码
咱们如今知道每次调用函数时,javascript 引擎都会建立一个新的执行环境,而如何建立这一系列的执行环境呢,答案是执行器会分为两个阶段来完成, 分别是建立阶段和激活(执行)阶段。而即便步骤相同可是因为规范的不一样,每一个阶段执行的过程有很大的不一样。bash
建立阶段:闭包
激活/执行阶段:函数
建立阶段:
激活/执行阶段:
咱们从规范上能够知道,ES3和ES5在执行环境的建立阶段存在差别,固然他们都会在这个阶段肯定this 的值 (关于this 的指向问题咱们之后会在专门的文章中分析各类this 的指向问题,这里便不作深究)。咱们将围绕这两个规范不一样点展开。尽管ES3的一些规范已经被抛弃,可是掌握ES3 建立执行环境的过程依然有助于咱们理解javascript深层次的概念。
VO 和 AO 是ES3规范中的概念,咱们知道在建立过程的第二个阶段会建立变量对象,也就是VO,它是用来存放执行环境中可被访问可是不能被 delete 的函数标识符,形参,变量声明等,这个对象在js环境下是不可访问的。而AO 和VO之间区别就是AO 是一个激活的VO,仅此而已。
变量对象(Variable) object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问可是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字对象属性的值对应它们的值但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象
激活对象(Activation object)有了变量对象存每一个上下文中的东西,可是它何时能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就能够被访问到了
如何建立VO对象能够大体分为四步
注意: 整个过程能够大概描述成: 函数的形参=>函数声明=>变量声明, 其中在建立函数声明时,若是名字存在,则会被重写,在建立变量时,若是变量名存在,则忽略不会进行任何操做。
一个简单的例子
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
复制代码
执行的伪代码
// 建立阶段
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
// 激活阶段
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
复制代码
词法环境和变量环境是ES5之后提到的概念,官方对词法环境的解释以下。
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
简单的理解,词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)。
ES3的VO,AO为何能够被抛弃?我的认为有两个缘由,第一个是在建立过程当中所执行的建立做用域链和建立变量对象(VO)均可以在建立词法环境的过程当中完成。第二个是针对es6中存储函数声明和变量(let 和 const)以及存储变量(var)的绑定,能够经过两个不一样的过程(词法环境,变量环境)区分开来。
词法环境由两个部分组成
对外部环境的引用关系到做用域链,以后再分析,咱们先来看看环境记录的分类。
环境记录分两部分
伪代码以下
// 全局环境
GlobalExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
···
}
outer: <null>
}
}
// 函数环境
FunctionExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
// 包含argument
}
outer: <Global or outer function environment reference>
}
}
复制代码
变量环境也是个词法环境,主要的区别在于lexicalEnviroment用于存储函数声明和变量( let 和 const )绑定,而ObjectEnviroment仅用于存储变量( var )绑定。
ES5规范下的整个建立过程能够参考下方的伪代码
let a = 20;
const b = 30;
var c;
function d(e, f) {
var g = 20;
return e * f * g;
}
c = d(20, 30);
复制代码
// 全局环境
GlobalExectionContext = {
this: <Global Object>,
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object", // 环境记录分类: 对象环境记录
a: < uninitialized >, // 未初始化
b: < uninitialized >,
d: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 环境记录分类: 对象环境记录
c: undefined, // undefined
}
outer: <null>
}
}
// 函数环境
FunctionExectionContext = {
this: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 环境记录分类: 声明环境记录
Arguments: {0: 20, 1: 30, length: 2}, // 函数环境下,环境记录比全局环境下的环境记录多了argument对象
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 环境记录分类: 声明环境记录
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
复制代码
前面讲建立过程当中,咱们留下了一个伏笔,ES3规范中有建立做用域链的过程,而ES5中在建立词法环境或变量环境的过程当中,也有生成外部环境的引用的过程。那这个过程有什么做用呢。咱们经过一个简单的例子来讲明。
function one() {
var a = 1;
two();
function two() {
var b = 2;
three();
function three() {
var c = 3;
alert(a + b + c); // 6
}
}
}
one();
复制代码
当执行到three 的执行环境时,此时 a和b 都不在c 的变量内,所以做用域链则起到了引用外部执行环境变量的做用。ES3中建立的做用域链如图:
当解释器执行alert(a + b + c),他首先会找自身执行环境下是否有a这个变量的存在,若是不存在,则经过查看做用域链,判断a是否在上一个执行环境内部。它检查是否a存在于内部,若找不到,则沿着做用域链往上一个执行环境找,直到找到,或者到顶级的全局做用域。同理ES6规范中也能够这样分析。
所以这会引入一个javascript一个重要的概念,闭包。从上面对执行环境的解释咱们能够这样理解,闭包就是内部环境经过做用域链访问到上层环境的变量。所以也存在没法进行变量回收的问题,只要函数的做用域链在,变量的值便由于闭包没法被回收。
注意: 此做用域链和原型链的做用域链不是同一个概念。
经过对javascript运行机制的介绍,对一些javasript高级概念有了更深的认识,特别是对一些云里雾里的概念区别有了更深入的认识。不一样规范下,不一样概念的解释更有利于深挖javascript底层的执行思想。我相信这是理解javascipt语言最重要的一步。
参考资料:
本文为博主原创文章,转载请注明出处 juejin.im/post/5c2052…