前言
本文翻译自what-is-the-execution-context-in-javascript
概述
当Javascript代码执行的时候,在哪个执行环境
中是非常重要的。它决定了当前作用域链
、VO(变量对象)
、this指向
。在本文中,目的就是为了更深入了解执行上下文,并且看完以后对解释器工作原理有个更清晰的认识
执行上下文
执行上下文形成一般有以下几种方式(不考虑es6块级作用域)
- Global Code :默认的全局环境(代码最开始执行的环境)
- Function Code: 函数内部环境
- Eval Code:暂不考虑
也就是说,在代码中存在两种环境,全局环境(作用域)和函数环境(作用域)。下面,看个例子(网络图)
代码中存在一个全局作用域
和对个函数作用域
,函数内可以获取全局作用域中变量,反之则不行。这和解析器中执行栈
有关系
执行栈
在浏览器中,Javascript是单线程的,也就是说同时只有一个事件发生,其他的事件暂时保存一个地方,这个地方就是执行栈
栈是数据结构中的一种,具有先进后出的特点,入栈出栈都是在栈的一端(栈顶)操作,效率高[时间复杂度为O(1)]
下图就是一个抽象的单线程栈图(网络图)
正如大家所知,浏览器第一次加载脚本的时候,默认就入的就是全局上下文,接着全局上下文入栈(一直在栈低)。当执行代码进入到内部函数的时候,又会创建一个新的上下文,接着新的上下文入栈(在栈顶)。而浏览器每次只会执行栈顶上下文(当前上下文),一旦执行结束,就被弹出栈,如此循环,知道栈清空为止。下面例子展示一个递归函数以及他的调用栈
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
执行栈中新的上下文入栈前,会记录当前上下文执行情况的相关信息,以便下次执行的时候继续执行
执行上下文细节
我们已经知道每当一个新的函数被调用,一个新的上下文会被创建并压入执行栈。然而在Javascirpt解析器内部,一个上下文创建有两个阶段
- 创建阶段(进入上下文阶段)
- 激活/执行代码(执行阶段)
1. 创建阶段
- 创建作用域链(chain scope)
- 创建变量,函数,arguments对象,参数
- 决定
this
指向
2. 激活/执行代码
- 变量赋值,函数引用,执行代码
从概念上,我们可以把上下文看成一个拥有三个属性的对象
executionContextObj = {
'scopeChain': {/*变量对象和父上下文的变量*/},
'variableObject': { /*函数arguments和参数,内部变量和函数声明*/ },
'this': {}
}
Variable Object(VO)
executionContextObj
对象当函数调用时候被创建,此时函数还没有执行。此时解析器创建executionContextObj
对象,并且扫描函数的传入的parameters(形参)和arguments(实参)、函数声明,变量声明,扫描结果都保存在executionContextObj
对象的variableObject
中
整个解析器执行代码步骤如下
-
- 找到当前调用函数的代码
-
- 在执行函数代码之前,创建执行上下文(execution context)
-
- 进入创近阶段
- 初始化作用域链
- 创建VariableObject对象
- 创建arguments对象,检查上下文找中的参数,初始化属性和属性值
- 扫描上下文中的函数声明
- 每找到一个函数声明,就在VariableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用
- 如果上述函数名已经存在于VariableObject下,那么对应的属性值会被新的引用所覆盖
- 扫描上下文中的变量声明
- 每找到一个变量声明,就在VariableObject下面用变量名建立一个属性,属性值为undefined
- 如果变量名已经存在VariableObject中,直接忽略
- 确定上下文中this指向
-
- 代码执行阶段
- 执行函数体中的代码,一行一行地运行代码,给VariableObject中的变量属性赋值
看看下面一个例子
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
当调用foo(22)
时,调用阶段executionContextObj
看起来如下
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
当创建阶段完毕,进入执行代码阶段,代码执行完后,executionContextObj
看起来如下
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
变量声明提升是怎么实现的
在一些文章中提到,在Javascipt中变量和函数声明会被提升到当前作用域顶端。但是并没有解释详细细节,但是此时我们应该是很清晰地知道原因。看下面示例代码
(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());
上面自执行函数调用的时候,就会创建执行上下文,例子中最上面可以访问foo以及bar变量,值分别为一个函数引用和undefined
- 为什么我们可以在声明f变量前就可以获取foo
- 因为在进入执行上下文创建阶段,代码还没有执行,这个时候就处理了arguments,函数声明和变量声明,被初始化他们的值,函数声明就在VariableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用,变量声明就在VariableObject下面用变量名简历一个属性,值为undefied。所以当执行代码的时候,其实foo和bar已经声明,所以可以拿到
- foo被声明了两次,为什么foo是执行一个函数而不是undefined
- 尽管foo被声明了两次,但是在
Variable Object
中函数声明先被创建,同时因为Variable Object
中如果属性名已经存在,会面声明就会被忽略,所以foo是函数引用,而不是undefind
- 尽管foo被声明了两次,但是在
总结
现在我们对Javascipt中解析器执行代码已经有一个很清晰的概念,也知道了执行上下文和执行栈,这样可以帮助我们写出更高质量的Javascipt代码
参考文档
- what-is-the-execution-context-in-javascript
相关文章
- chapter-2-variable-object/
- chapter-2-variable-object中文版/
- Identifier Resolution, Execution Contexts and scope chains