Execution Context的概念
在JavaScript中,当前代码运行的环境被称为 执行上下文(Execution Context) ,或者 作用域(Scope) 。在客户端中,JavaScript是以单线程的方式运行的,所以某个时间点它只能在下面三个Execution Context中的一个中运行:
- Global Execution Context
- Functional Execution Context
- Eval Execution Context
Functional Execution Context是当执行流程进入函数体内时代码执行环境,Eval Execution Context是通过
eval()
函数执行代码时的代码执行环境。
每次调用函数时都会创建一个Functional Execution Context,而Global Execution Context只有一个。
Execution Context Stack
在客户端中,JavaScript是以单线程的方式运行的,所以某个时间点它只能做一件事情,其他的操作或者事件只能排队等待。
比如下面的脚本
function first() {
console.log('Welcome to the first room ...');
second();
function second() {
console.log('Welcome to the second room ...');
third();
function third() {
console.log('Welcome to the third room ...');
}
}
}
first();
当在Global Execution Context中执行
first();
这句时,会依次调用三个嵌套的函数:
first()
、
second()
和
third()
函数调用的过程 :
函数调用的过程其实是一个入栈的过程,每次调用一个函数就会创建一个Functional Execution Context,然后加入到Execution Context Stack的顶部。
函数执行返回的过程 :
函数执行返回的过程其实是一个出栈的过程,一旦一个Execution Context完成了执行,它就会被推出栈顶并且将控制权返回给它下面的Execution Context,直到Global Execution Context。
函数执行返回、函数执行、函数调用
函数执行返回 :是指函数内的语句(包括调用其他函数/方法)全部执行完成,并且返回调用该函数的地方
函数执行 :包括执行该函数内部普通语句(即 代码执行 ),也包括调用其他函数/方法(即 函数调用 )
可以看出,Execution Context Stack具有如下的特点:
- 单线程
- 同步执行
- 只有一个Global Execution Context
- 可以有多个Functional Execution Context,每次调用一个函数都会创建一个Functional Execution Context
JavaScript解释器(JavaScript Interpreter)内部如何操作Execution Context
在JavaScript解释器的内部,每一个Execution Context的执行都会经历以下两个阶段:
- 创建阶段(Creation Stage)
- 激活/运行阶段(Activation Stage / Code Execution Stage)
创建阶段
主要是指,函数被调用了,但是还没有开始执行函数内部的代码。在Execution Context的创建阶段,解释器会做如下的一些工作:
- 创建作用域链(Scope Chain)
- 创建Activation Object / Variable Object
-
确定
this
的值,即Context
如以下代码
function add(num1, num2){
return num1 + num2;
}
var result = add(5, 10);
调用函数
add()
将创建一个Execution Context,如图所示:
在Execution Context的创建阶段,JavaScript解释器(JavaScript Interpreter)会扫描传入函数的参数、函数内的变量声明和函数声明,从而在Execution Context中创建一个与之关联的Variable Object。被扫描到的变量、参数和函数将作为Variable Object的属性存在(见图)。
创建Execution Context的大致的过程可以描述为:
- 调用函数(Invoke function)
-
在执行函数内的代码之前,创建Execution Context
- 初始化作用域链
-
创建Variable Object,按顺序执行:
-
创建
arguments
对象,并且检查该函数的参数,完成对形式参数的赋值 - 在该函数内扫描函数声明,在Variable Object中创建或覆盖相应的属性
-
在该函数内扫描变量声明,在Variable Object中创建响应的属性,
并且将值初始化为
undefined
。如果在Variable Object中已经存在与声明的变量同名的属性,则什么都不做,继续扫描下一个变量声明。
-
创建
-
确定
this
的值,即Context
比如
function foo(name) {
var a = 'hello';
var b = function f2() {};
function f3() {}
}
foo('River He');
调用
fn('River He')
,创建如下所示的Execution Context:
fooExecutionContext = {
scopeChain: {...},
variableObject: {
arguments: {
: 'River He',
length: 1
},
name: 'River He',
f3: pointer to function f3(),
a: undefined,
b: undefined
},
}
可见,在这一阶段除了函数的参数会进行赋值外,对于该函数内部的变量则只会在Variable Object中定义属性名。
激活/运行阶段
对VariableObject中的属性进行赋值,解释执行函数内的代码。到这一阶段,上述例子中的Execution Context会变成:
fooExecutionContext = {
scopeChain: {...},
variableObject: {
arguments: {
: 'River He',
length: 1
},
name: 'River He',
f3: pointer to function f3(),
a: 'hello',
b: pinter to function f2()
},
}
Scope Chain
在代码运行期间,标识符解析(即变量名和函数名的查找)是通过当前Execution Context的Scope Chain进行的。
对于当前的Execution Context:
- 它本身的Variable Object就是Scope Chain的第一个元素
-
在Execution Context Stack中仅次于
当前Execution Context
的那个Execution Context的Variable Object为它的Scope Chain中的第二个元素,依次类推,直到Global Execution Context的Variable Object。
About Hoisting
对于如下的代码,会发生ReferenceError
(function() {
// ReferenceError: Can't find variable: name
console.log(name);
}());
而对于一下代码,虽然变量
name
是在
console.log(name)
后声明的,但却不会发生ReferenceError:
(function() {
// undefined
console.log(name);
// declare variable 'name'
var name = 'River He';
}());
表面看起来好像是变量声明
提前(hoisted)
了,类似这样:
(function() {
var name;
// undefined
console.log(name);
name = 'River He';
}());
其实不然,这是因为:
-
在JavaScript解释器解释执行函数内的代码之前:需要创建Execution Context,在创建Execution Context的时候对于函数内的变量声明
var name = 'River He';
进行了扫描,并且在Variable Object中创建了相应的属性。 - 在JavaScript解释器解释执行函数内的代码的时候:需要对遇到的变量做解析,解析的过程就是依次查找当前Execution Context的Scope Chain中的各个Variable Object的属性。根据第1步,可以找到相应的属性,因而不会发生ReferenceError。
至于为什么是
undefined
,则是因为在Execution Context的创建阶段,对于函数内的变量声明,JavaScript解释器会在Variable Object中创建相应的属性,并且初始化为undefined.
同理,以下的惯用法中,即便变量
name
没有事先声明,也不会发生ReferenceError
var name = name || 'River He';
对于如下代码
(function() {
console.log(typeof foo);
var foo = 'bar';
function foo() {}
}())
它的输出是
function
而不是
undefined
,这是因为Execution Context创建阶段,函数内的函数声明是优先于变量声明被JavaScript解释器扫描并且在Variable Object中创建相应的属性的。
因此Variable Object中首先有个名为
foo
的属性,并且它的值是一个Function类型对象的引用。在扫描完函数声明后,JavaScript解释器接着扫描变量声明,扫描到
var foo = 'bar';
的时候,因为Variable Object中已经存在了名为
foo
的属性,因此JavaScript会什么都不做,继续往下扫描。
Global Variable
在Global Execution Context中的全局对象就是Global Execution Context中的Variable Object,并且为
this
所引用。
显式声明
在Global Execution Context中使用了
var
声明的变量就是显式声明的全局变量。显式声明的全局变量本质上是Global Execution Context中Variable Object的一个属性,并且这个属性不能被删除。
// 显式声明全局变量
var name;
// 删除全局对象中的"name"属性,false
console.log(delete this.name);
这是因为如果一个全局变量能够删除,那么可能会给整个JavaScript程序造成影响。通过将全局对象中与显式声明的全局变量同名的属性的configurable属性设置为false,让显式声明的全局变量不可删除。
var name = 'River He';
console.log(Object.getOwnPropertyDescriptor(this, 'name'));
输出
隐式声明的全局变量
无论在Global Execution Context中还是在Functional Execution Context中,没有使用
var
声明的变量都是全局变量。
如
name = 'River He';
或者
function fn() {
name = 'River He';
}
在Execution Context的创建阶段,这甚至不会被认为是变量声明,而是变量赋值。事实上也确实如此。也就是说,在创建Excution Context的Variable Object,JavaScript解释器扫描函数内的变量声明的时候,并不会为上述例子中的
name
变量在Variable Object中创建相应的属性。
因此,当开始执行Execution Context中的代码,对变量
name
进行解析,查找Scope Chain的时候会一直查找到Global Execution Context。当在Global Execution Context中依然无法查找到时,此时就会在Global Execution Context中的Variable Object中创建相应的属性。
虽然这看起来确实是一个全局变量,但是严格说起来它不是,因为全局对象从设计的角度就不应该是可删除的,而这里的"全局变量"是可以被删除的:
name = 'River He';
// true
console.log(delete this.name);
查看全局对象中相应属性的
configurable
属性:
name = 'River He';
console.log(Object.getOwnPropertyDescriptor(this, 'name'));
输出
在ECMAScript 5的
strict mode
中,通过这样的方式声明全局变量是不合法的。从性能的角度来说,也不应该使用这种方式。