执行上下文(ES3)
这里介绍的执行上下文,是 ES3 概念。
1 Js 的执行过程:
提到的知识点:
编译时:词法作用域、函数作用域、提升、作用域链、变量对象 VO;
运行时:执行上下文、活跃的变量对象 AO、this。
在进一步阅读本文之前,可以先回顾一下相关篇章:“Js 的执行过程”。
在 JavaScript 引擎内部,每次调用执行上下文,会分为两个阶段:
编译阶段,有人称创建执行上下文阶段,此时函数被调用,但未执行任何其内部代码:
创建 scopeChain 作用域链,保存该函数可访问的 AO 变量对象(包括自身)。
创建 activation object(AO) 变量对象,保存内部的变量、函数声明、函数参数 arguments。
确定 this 的指向。
运行阶段:
- 指派变量的值和函数的引用,解释/执行代码。
在编译时,会创建执行上下文,主要是做了下面三件事:
- 创建作用域链(scope chain);
- 创建变量对象(variable object);
- 确定 this 的指向。
每个执行上下文会形成如下的结构:
executionContextObj = {
scopeChain: [ /* AO + 所有父执行上下文的变量对象*/ ],
variable object: { /* arguments、变量、函数声明 */ },
this: xxx
}
2 变量对象 variable object
在确定作用域时,会涉及到一个数据结构,即 VO 变量对象。每个作用域内,都会有一个变量对象,这里面保存了该作用域中所有的变量和函数声明。
也就是说,变量对象其实是一个可以查询的表,登记了作用域范围内所有的变量和函数。让程序可以快速的了解当前作用域内部,有哪些变量和函数可以访问。
- 变量对象 VO,只保存了自身作用于范围内部的可访问变量和函数。它不保存外部的父作用域,自身的子作用域内部的数据。
- VO 同该函数一齐保存在堆内存中。
创建变量对象主要是经过以下过程:
创建 arguments 对象。以 K/V 的形式存储。key 为从0开始的数字,value 为
undefined
(变量),地址值(函数);检查当前上下文的函数声明。以 K/V 的形式存储。key 为函数名称,value 为函数的地址值,指向堆内存中的那个函数。
- 如果存在同名函数声明,则后出现的函数声明会覆盖先出现的。
检查当前上下文的变量声明。以 K/V 的形式存储。key 为变量名称,value 为
undefined
。- 如果存在同名的变量声明,不会出现覆盖问题(因为变量声明的值都是
undefined
。 - 如果存在同名的函数声明,则忽略变量声明。
- 如果存在同名的变量声明,不会出现覆盖问题(因为变量声明的值都是
注意优先级:函数声明 > 函数的参数 > 变量声明
其基本结构的伪代码如下:
VO:{
arguments 函数的参数,
函数(FunctionDeclaration, 缩写为FD),
变量(var 声明的变量)
}
举例:
var a = 10;
function test(x) {
var b = 20;
};
foo(30);
其在编译阶段,解析的变量对象有两个,分别是:
// 全局作用域的变量对象
globalVO = {
foo: pinter to foo function,
a: undefined
};
// foo函数作用域的变量对象
fooVO = {
arguments: {
0: undefined,
length: 1
},
x: undefined,
b: undefined
};
3 活动对象 activation object
activation object 活动对象,是相对 变量对象 VO 来描述的。
创建变量对象(VO)发生在编译时,还没有进入到运行时,此时的变量对象中的变量属性尚未赋值,值仍为undefined。只有运行时,变量中的变量属性才进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,才能进行访问,这个过程就是VO -> AO过程。
- VO 是在编译时,收集当前作用域内可访问的所有变量和函数信息。
- AO 是在运行时,基于 VO 转变而来的。
AO 的作用 和 VO 大致相同,保存了该作用域中所有的变量和函数声明。
也就是说,活动对象也是一个可以查询的表,登记了作用域范围内所有的变量和函数。让程序可以快速的了解当前作用域内部,有哪些变量和函数可以访问。
其基本结构的伪代码如下:
AO:{
函数的参数,
函数(FunctionDeclaration, 缩写为FD),
变量(var 声明的变量)
}
举例:
var a = 10;
function test(x) {
var b = 20;
};
foo(30);
在运行时,当运行到 foo(30)
,
在编译阶段,是变量对象 variable object
// foo函数作用域的变量对象
fooVO = {
arguments: {
0: undefined, // 注意,这里和 AO 不同
length: 1
},
x: undefined,
b: undefined
};
进入运行阶段时,是活动对象 scope object
// foo函数作用域的活动对象
fooAO = {
arguments: {
0: 30 // 注意,这里和VO不同,VO是undefined。
length: 1
},
x: 30
b: undefined // 如果代码执行到相关赋值语句,这里的值也会发生变化。
};
可以看到,和编译时的 VO 的不同,就是函数的参数 arguments 的变量,在运行时刚刚开始的时候就已经赋值了。因为运行时传递了函数参数的缘故,AO 是有具体变量值,而 VO 是 undefined
。然后,随着代码的一步步执行,AO 内变量和函数的具体值还会更新。
4 作用域 scope
在编译阶段,会对所有的变量和函数进行提升,也就是变量和函数声明的提前编译。提前编译的核心目的,就是为了确定各自的作用域,形成作用域链。
function foo() {
function bar() {
...
}
}
在编译时,会对这些函数声明进行解析,解析后形成 [[scope]]
保存了函数自身可访问到的所有 父作用域,形成了一个 “作用域链”。
- 此时并不是一个完整的作用域链,因为该作用域链只保存了父作用域的 VO,未保存把自身的 VO 也加入进来。
- 作用域链(scope chain)当于
VO + [[scope]]
。
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.VO,
globalContext.VO
];
可以看到, [[scope]]
就是把所有可访问的父作用域 VO 全部按顺序保存进来。
5 作用域链 scope chain
作用域链由当前执行环境的变量对象 VO(未进入到运行时)与上层环境的一系列活动对象 AO (运行时)组成。
var num = 30;
function foo() {
var a = 10;
function innerFoo() {
var b = 20;
return a + b
}
innerTest()
}
test()
innerFoo 的执行上下文,其中包含了它的作用域链:
innerFooExecutiveContext = {
//变量对象
VO: {b: undefined},
//作用域链
scopeChain: [innerFooContext.VO, fooContext.AO, globalContext.AO],
//相当于:
scopeChain: [VO, ...[[scope]]],
//this指向
this: window
}
使用数组表示作用域链,其内容的活动对象、变量对象就是一个个作用域。
- 作用域链中最后一个成员,是当前作用域(当前上下文的变量对象(编译时)、活动对象(运行时);
- 作用域链中的第一个成员,是全局作用域(全局上下文的活动对象);
作用域链保证了变量和函数的 有序访问,查找方式是沿着作用域链,从尾至头查找变量或函数。
- 如果找到了索要寻找的变量或函数,便停止查找,返回找到的key/value(变量或函数名称 / 值)
- 如果是 LHS 左查找,则返回变量或函数的 key;
- 如果是 RHS 右查找,则返回变量或函数的 value。
- 一直向上查找至全局作用域,如果在全局作用域中也无法找到,则停止查找,抛出错误。
6 执行上下文栈 ECStack
执行上下文栈也成为调用栈。
调用栈是一个机制,用来让 JavaScript 引擎方便地去追踪函数执行。当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行,以及查看各函数之间的调用关系。
JS 的 运行环境 主要有 3 种:
- 全局环境(在开始执行代码时,最先进入的就是全局环境);
- 函数环境(函数调用的时候,进入到该函数环境);
- eval环境(存在安全和性能问题)。
由于浏览器里的 JavaScript 解释器是单线程的。和 Java 一样,Javascript 在执行时也是一个栈的结构:
每进入到一个不同的运行环境,都会创建一个相应的 执行上下文(execution context)。
JS 引擎会以栈的数据结构对这些执行上下文进行处理,形成执行上下文栈(ECStack)。
栈底永远是 全局执行上下文(global execution context),栈顶则永远时当前的执行上下文。
- 全局执行上下文,在运行时会最先被放入栈底,且一直存在,直到代码全部执行完毕才会出栈。
- 活跃指针:指针会一直指向栈的最顶端,也是此时正活跃的执行上下文。“活跃” 指的是,当前正在执行的代码,其所处的执行上下文。
- 入栈:当正在执行的代码中,出现函数调用,即会对应的创建一个新的执行上下文,把该执行上下文压入栈中。活跃指针指向了这个新建的执行上下文。开始执行新执行上下文中的代码。
- 出栈:当执行上下文中的代码全部执行完毕,执行上下文会从栈中弹出。活跃指针指向上一个执行上下文。
另外:JavaScript 的引用数据存储,是在一个堆结构中。包括 function、 array 等各种对象,都保存在堆内存中。栈中的变量中,保存的是这些数据在堆内存的地址。所以才会把引用数据类型的值,称之为”地址值“。相反,基本数据类型的值,就是具体的值。因为 string、number 等数据,都是直接保存在栈中的(具体来讲,是栈中,每一个执行上下文中的 VO / AO)。
举个例子:
function foo1() {
console.log("foo1")
foo2()
}
function foo2() {
console.log("foo2")
foo3()
}
function foo3() {
console.log("foo3")
}
foo1()
先判断一下这里在编译时的词法作用域:
- 全局作用域中:保存了 2 个函数声明,foo1,foo2;
- foo1 的函数作用域:保存了 1 个函数声明,foo2;
- foo2 的函数作用域:内部没有变量和函数声明。
然后,在运行时,会创建执行上下文堆栈:
- 默认压入全局执行上下文
global execution context
; - 当执行到
foo1()
时,出现函数调用,此时创建并压入foo1 execution context
。活跃指针指向这个新创建的执行上下文,开始执行里面的代码。 - 当执行到
foo2()
时,出现函数调用,此时创建并压入foo2 execution context
。活跃指针指向这个新创建的执行上下文,开始执行里面的代码。
此时执行上下文堆栈,从下至上依次是:global execution context
--> foo1 execution context
--> foo2 execution context
。
- 当
foo2()
执行完毕时,其对应的执行上下文会弹出栈。此时活跃指针指向了foo1 execution context
,继续执行foo1 execution context
中的代码; - 当
foo1()
执行完毕时,其对应的执行上下文会弹出栈。此时活跃指针指向了global execution context
,继续执行global execution context
中的代码; - 全部代码执行完毕。
注:
- 全局上下文在运行时首先被压入栈中,且一直存在,直到全部代码执行完毕。
- 每次函数被调用创建新的执行上下文,包括调用自身。
6.1 如何查看上下文栈
- 设置
debugger
,然后利用浏览器开发者工具 - call stack。 - 设置
console.trrace()
,输出当前函数调用关系。
7 执行上下文的步骤(ES3)
下面进一步说明每一个小阶段所要做的事情:
当代码执行到某一个函数调用,如 foo(name, 1);
会创建一个新的执行作用域,并压入栈中,此时:
进入编译阶段,创建一个 foo 执行上下文对象 fooExecutionContextObj
,并其内容:
- 初始化作用域链。通过向父作用域访问,获取到当前 foo 函数可访问到的所有变量对象 AO。可以用一个数组结构理解,先写入当前作用域中的所有变量对象 VO,最后形成:
[VO, ...[[scope]]]
。 - 创建变量对象 VO,并扫描当前执行上下文中的:
- 扫描 函数参数 arguments,写入 VO 中;
- 以 K/V 的形式存储,Key 为变量名称,Value 为
undefined
。
- 以 K/V 的形式存储,Key 为变量名称,Value 为
- 扫描当前执行上下文中的 函数声明,并写入 VO 中。
- 以 K/V 的形式存储,Key 为函数名称,Value 为函数地址值。
- 若出现同名函数,则后出现的会覆盖前面的。
- 扫描当前执行上下文中的 变量声明,并写入 VO 中。
- 以 K/V 的形式存储,Key 为变量名称,Value 为
undefined
。 - 若已存在同名函数,则忽略变量声明。也就是说函数声明不会被覆盖。
- 以 K/V 的形式存储,Key 为变量名称,Value 为
- 扫描 函数参数 arguments,写入 VO 中;
- 求出当前执行上下文中的 this 。
在运行时,当前执行上下文的变量对象 VO 转化为引擎可访问的 活动对象 AO。然后对代码进行一行行的执行,并会查询 / 更新 AO 的内容。
- 注意:函数声明优先级 > 函数的参数 > 变量声明
举例:
var name = "Moxy";
function foo(name, number) {
var age = 18;
var sex = "male";
function callName() {
// ...
}
};
foo(name, 1);
当调用 foo(name, 1);
时,执行上下文对象在创捷阶段完成后如下:
fooExecutionContext = {
scopeChian: [variableObject, {...}], // 作用域链的内容,见“作用域链”章节
variableObject: {
arguments: {
0: undefined,
1: undefined,
length: 2
},
callName: <pointer to function callName()>,
name: undefined,
number: undefined,
age: undefined,
sex: undefined,
},
this: window;
}
该行代码在执行完函数后,被销毁前, foo 的执行上下文对象是这个样子的:
fooExecutionContext = {
scopeChian: [variableObject, {...}], // 作用域链的内容,见“作用域链”章节
variableObject: {
arguments: {
0: "Moxy",
1: 1,
length: 2
},
name: "Moxy",
number: 1,
callName: <pointer to function callName()>,
age: 18,
sex: "male",
},
this: window;
}