执行上下文、作用域链和JS内部机制(Execution context, Scope chain and JavaScript internals)
一、执行上下文
执行上下文(Execution context EC)是js代码的执行环境,它包括this的值、变量、对象和函数。
js执行上下文有3种类型
1. 全局执行上下文(Global execution context GEC)
全局上下文是文件第一次加载到浏览器,js代码开始执行的默认执行上下文。在浏览器环境中,严格模式下this的值为undefined,否则this的值为window对象。GEC只能有一个(因为js执行的全局环境只能有一个)。
2. 函数执行上下文(Functional execution context FEC)
函数执行时创建函数执行上下文,每个函数都有自己的执行上下文。FEC可以获取到GEC中的内容。当在全局上下文中执行代码时js引擎发现一个函数调用,则创建一个函数执行上下文。
3. Eval
执行eval时创建
二、执行上下文栈
执行上下文栈Execution context stack (ECS)是执行js代码时创建的执行栈结构。GEC默认在栈的最里层,当js引擎发现一个函数调用,则创建这个函数的FEC并push进栈,js引擎执行栈顶上下文关联的函数,一旦函数执行完,则将其FEC pop出栈,并往下执行。
看个例子(动图插不了栈动图链接)
var a = 10;
function functionA() {
console.log("Start function A");
function functionB(){
console.log("In function B");
}
functionB();
}
functionA();
console.log("GlobalContext");
- 当上面的代码在浏览器中加载时,js引擎先将GEC push入ECS中,当在GEC中调用functionA时,functionA执行上下文被push入栈,并开始执行functionA。
- 当functionB在functionA中被调用时,functionB的执行上下文被push入栈,开始执行functionB,当functionB中内容执行完,functionB执行上下文被pop出栈,此时栈顶为functionA的执行上下文,继续执行functionA的代码,执行完后pop出栈,栈顶为GEC。
- 最终执行GEC中代码,执行完pop整个代码结束。
上面讨论了js引擎如何处理执行上下文(push和pop),下面讨论js引擎如何创建执行上下文,这个过程分为两个阶段:创建阶段和执行阶段。
三、创建执行上下文
1. 创建阶段(后面又叫编译阶段)
js引擎调用函数,但函数还没开始执行阶段。
js引擎在这个阶段对整个函数进行一个编译(compile the code),主要干了下面三件事:
(1) 创建Activation object 或 the variable object(后面就简称它可变对象吧,不知道有没有专业的中文名)
可变对象是包含所有变量、函数参数和内部函数声明信息的特殊对象,它是一个特殊对象且没有__proto__属性。
(2)创建作用域链
一旦可变对象创建完,js引擎就开始初始化作用域链。作用域链是一个当前函数所在的可变对象的列表,其中包括GEC的可变对象和当前函数的可变对象。
(3)决定this的值
初始化this的值
下面通过一个例子进行说明
function funA (a, b) {
var c = 3;
var d = 2;
d = function() {
return a - b;
}
}
funA(3, 2);
当调用funA和执行funA前的这段时间,js引擎为funA创建了一个executionContextObj如下
executionContextObj = {
variableObject: {}, // All the variable, arguments and inner function details of the funA
scopechain: [], // List of all the scopes inside which the current function is
this // Value of this
}
可变对象包含参数对象(包含函数参数的细节),声明的变量和函数,如下所示
variableObject = {
argumentObject : {
0: a,
1: b,
length: 2
},
a: 3,
b: 2
c: undefined,
d: undefined then pointer to the function defintion of d
}
- argumentObject如上所示
- 函数中的变量会被初始为undefined,参数也会在可变对象中呈现
- 如果变量在参数对象中已存在,js引擎选择忽略
- js引擎在当前函数中遇到函数定义,会用函数名创建一个属性指向函数定义存储的堆内容
2. 执行阶段
在此阶段,js引擎会重扫一遍函数,用具体的变量的值来更新可变对象,并执行代码内容。
执行阶段执行完后,可变对象的值如下:
variableObject = {
argumentObject : {
0: a,
1: b,
length: 2
},
a: 3,
b: 2,
c: 3,
d: undefined then pointer to the function defintion of d
}
四、完整的例子
代码如下
a = 1;
var b = 2;
cFunc = function(e) {
var c = 10;
var d = 15;
a = 3
function dFunc() {
var f = 5;
}
dFunc();
}
cFunc(10);
全局编译阶段
当浏览器加载上面的代码后,js引擎进入编译阶段,只处理声明,不处理值。下面走读一遍代码:
- a被赋值1,但它并不是个变量或函数声明,js引擎在编译阶段什么都不做;
- b变量声明初始化为undefined;
- cFunc函数声明初始化为undefined。
此时的
globalExecutionContextObj = {
variableObject: { // 原文中有时用activationObj
argumentObj : {
length:0
},
b: undefined,
cFunc: Pointer to the function definition
},
scopeChain: [GLobal execution context variable object],
this: value of this
}
全局执行阶段
再接着上面,js引擎进入执行阶段并再过一遍。此时将会更新变量名和执行
- js引擎发现可变对象中没有a属性,因此在GEC中添加a属性,并初始化为1;
- 可变对象有b,直接更新b的值为2;
- 接着是函数声明,不做任何事;
- 最后调用cFunc,js引擎再次进入编译阶段创建一个cFunc的执行上下文。
此时
globalExecutionContextObj = {
variableObject: {
argumentObj : {
length:0
},
b: 2,
cFunc: Pointer to the function definition,
a: 1
},
scopeChain: [GLobal execution context variable object],
this: value of this
}
cFunc的编译阶段
由于cFunc有个参数e,js引擎会在cFunc执行上下文对象可变对象添加e属性,并初始化为2
- js引擎查看cFunc执行上下文的可变对象没有c,因此添加c,并初始化为undefined,d类似;
- a = 3非声明,跳过;
- 函数声明,创建dFunc属性指向函数的堆空间;
- 对dFunc执行语句忽略
此时
cFuncExecutionContextObj = {
activationbj: {
argumentObj : {
0: e,
length:1
},
e: 10,
c: undefined,
d: undefined
dFunc: Pointer to the function definition,
},
scopeChain: [cFunc variable object, Global exection context variable object],
this: value of this
}
cFunc的执行阶段
- c和d获取到初始化值;
- a不是cFunc执行上下文对象中的属性,js引擎会在作用率链的帮助下转到GEC(全局执行上下文),查找a是否在GEC中。如果不存在,则会在当前作用域创建并初始化它;如果GEC中有,则更新其值,这里会更新为3。js引擎只有在发现一个变量在当前执行上下文对象属性中找不到时会跳转到GEC中;
- 创建dFunc属性并指向函数的堆内存
cFuncExecutionContextObj = {
activationbj: {
argumentObj : {
0: e,
length:1
},
e: 10,
c: 10,
d: 15
dFunc: Pointer to the function definition,
},
scopeChain: [cFunc variable object, Global exection context variable object],
this: value of this
}
调用dFunc,js引擎再次进入编译阶段,创建dFunc执行上下文对象。
dFunc执行上下文对象可以访问到cFunc和全局作用域中的所有变量和函数;同样cFunc可以访问到全局的,但不能访问dFunc中的;全局上下文对象不能访问cFunc和dFunc中的变量和对象。
有了上面的概念,对hoisting(变量提升)应该更容易理解了。
五、作用域链
作用域链是当前函数所在的可变对象列表
看下面一段代码
a = 1;
var b = 2;
cFunc = function(e) {
var c = 10;
var d = 15;
console.log(c);
console.log(a);
function dFunc() {
var f = 5;
console.log(f)
console.log(c);
console.log(a);
}
dFunc();
}
cFunc(10);
当cFunc被调用时,cFunc的作用域链如下
Scope chain of cFunc = [ cFunc variable object,
Global Execution Context variable object]
当dFunc被调用时,dFunc在cFunc中,dFunc的作用域链包含dFunc、cFunc和全局可变对象
Scope chain of dFunc = [dFunc variable object,
cFunc variable object,
Global execution context variable object]
当我们尝试访问dFunc中的f,js引擎查看f是否可从dFunc的可变对象中获取,找到console输出;
访问c变量,js引擎首先在dFunc的可变对象中获取,不能获取,则到cFunc的可变对象中去获取,找到console输出;
访问a变量,同上,最后找到GEC的可变对象,获取到并console输出
同样,cFunc中获取c和a类似
在cFunc中访问不到f变量,但dFunc中可以通过作用域链获取到c和d
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。