|
JS代码的执行过程分为两个阶段,编译阶段与执行阶段。
编译阶段 |
词法、语法分析 |
主要就是将源代码转换成机器内部识别的符号表(token table )。
token类别:
- 标识符 x, y1, ……
- 常数 1,12,123,12.3
- 以下的类别中,每一个示例都可以被看做一个种类
- 关键字(保留字) var, undefiend…
- 操作符 +,-,/,*,<,>,……
- 分隔符 ;, {, } , ……
可执行代码生成 |
主要就是将目标代码连接,然后形成机器能够直接执行的代码。
目标代码:计算机科学中编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成。
作用域确认 |
确认代码作用域的归属。属于全局作用域,函数作用域或是块级作用域。
阶段主要任务:
变量名以及函数(function具名函数)名提取到标识符表,变量只作声明,不作初始化。
将代码翻译成可执行代码,这个阶段确定作用域规则。
执行阶段 |
- 进入上下文执行生命周期
- 代码执行
- 垃圾回收
|
执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。那么JavaScript中的运行环境有几种呢?
可执行代码的运行环境 |
-
全局环境
JavaScript代码运行起来会首先进入该环境。 -
函数环境
存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文 -
eval
指运行在eval函数的代码,很少用而且不建议使用,有安全,性能等问题
上下文对象 |
每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
this
全局上下文对象:
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
变量对象 |
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。(全局上下文中的变量对象就是全局对象,浏览器中就是window
对象)
变量对象 VO 与 活动对象 AO 关系 |
VO 和 AO 是同一个对象,是处于执行上下文的不同生命周期的称呼。
- 未进入执行阶段之前,变量对象(VO)中的属性都不能访问!
- 进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
变量对象属性 |
📌执行上下文创建阶段,变量对象会包括:
1.函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 若没有实参,属性值设为
undefined
2.函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
3.变量声明
- 由名称和对应值(
undefined
)组成一个变量对象的属性被创建; - 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
⚖ 在进入执行上下文后,VO→AO,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
📌执行上下文执行阶段,变量对象会包括:
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
还是上面的例子,当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
变量对象创建过程 |
-
全局上下文的变量对象初始化是全局对象
-
函数上下文的变量对象初始化只包括 Arguments 对象
-
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
-
在代码执行阶段,会再次修改变量对象的属性值
作用域链 |
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数有一个内部属性 [[scope]]
,当函数创建的时候,就会保存该函数所有父变量对象到其中。
<script>
function foo() {
function bar() {
...
}
}
</script>
函数创建时,各自的[[scope]]
为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
当函数激活时,进入函数上下文,创建 VO/AO 后,创建函数上下文对象,将活动对象添加到作用链的前端。
barContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, fooContext.AO,globalContext.VO]
}
函数定义时候的[[scope]]
和函数执行时候的Scope
区别,前者作为函数的属性,后者作为函数执行上下文的属性。
查看scope属性
function test(){
function foo(){
console.dir(foo)
}
foo()
}
test()
执行上下文生命周期 |
创建阶段 |
- 生成变量对象
- 建立作用域链
- 确定
this
指向
执行阶段 |
- 变量赋值
- 函数引用
- 执行其他代码
执行上下文栈 |
除了全局代码外,还有许多调用的函数,每个函数的调用都会创建一个执行上下文,调用完就会被回收。这些临时上下文是如何管理的呢?
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext
表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext
。
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
执行上下文特点:
- 单线程
- 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈
- 函数的执行上下文的个数没有限制
- 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。
如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。
|
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
执行过程如下:
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [
globalContext
];
2.全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
4.checkscope 函数执行上下文初始化:
- 复制函数 [[scope]] 属性创建作用域链,
- 用 arguments 创建活动对象,
- 初始化活动对象,即加入形参、函数声明、变量声明,
- 将活动对象压入 checkscope 作用域链顶端
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
ECStack = [
fContext,
checkscopeContext,
globalContext
];
6.f 函数执行上下文初始化, 以下跟第 4 步相同:
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
}
7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
ECStack = [
checkscopeContext,
globalContext
];
9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
注意:
this
是在函数执行的时候才确定下来的,checkscope 函数 和 f 函数的 this 的值跟作用域链没有关系。两者的 this
其实都是 undefined
,只是在非严格模式下,会转为全局对象。this
具体的取值规则:JavaScript深入之从ECMAScript规范解读this
参考文档: