JavaScript 执行原理
词法作用域 #
JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
let value = 1
function foo() {
console.log(value)
}
function bar() {
let value = 2
foo()
}
bar()
2
3
4
5
6
7
8
9
- 假设 JavaScript 采用静态作用域,让我们分析下执行过程:
- 执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于1,所以结果会打印1。
- 假设 JavaScript 采用动态作用域,让我们分析下执行过程:
- 执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value,如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印2.
- JavaScript 采用的是静态作用域,所以这个例子的结果是1。
执行上下文 #
执行上下文(Execution Context)是 JavaScript 中最基础但最重要的一个概念。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。 JavaScript 中的运行环境包括以下三种情况:
- 全局环境:JavaScript代码运行起来会首先进入该环境,即全局执行上下文
- 函数环境:当函数调用执行时,进入函数执行上下文
- eval:不建议使用,可忽略
因此在一个 JavaScript 程序中,必定会产生多个执行上下文,JavaScript 引擎会以栈的方式来处理它们,这个栈,称为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。当代码在执行过程中,遇到以上三种运行环境,都会产生一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈。
function fun3() {
console.log('fun3')
}
function fun2() {
fun3()
}
function fun1() {
fun2()
}
fun1()
2
3
4
5
6
7
8
9
10
以上例子执行过程如下:
- 全局执行上下文入栈,ECStack.push(Global Context);
- 执行fun1(),创建fun1的执行上下文并入栈,ECStack.push(<fun1> Function Context);
- fun1的执行上下文入栈后,开始执行fun1函数中的可执行代码,fun1中调用了fun2,创建fun2的执行上下文并入栈,ECStack.push(<fun2> Function Context);
- 执行fun2函数中的可执行代码,fun2调用了fun3,ECStack.push(<fun3> Function Context);
- fun3执行完毕,fun3的执行上下文出栈,ECStack.pop();
- fun2执行完毕,ECStack.pop();
- fun1执行完毕,ECStack.pop();
- 最终ECStack中只剩下全局上下文,全局上下文在浏览器窗口关闭后出栈。
当调用一个函数时(激活),一个新的执行上下文就会创建。而一个执行上下文的生命周期可以分为两个阶段:
- 创建阶段:创建变量对象,建立作用域链,确定this的指向
- 代码执行:变量赋值,函数引用,以及执行其它代码
变量对象 #
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明:
- 全局上下文中的变量对象就是全局对象。
- 在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
- 活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在JavaScript环境中访问,只有当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
- 活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。
变量对象包括:
- 函数的所有形参(如果是函数上下文):
- 由名称和对应值组成的一个变量对象的属性被创建;
- 没有实参,属性值设为undefined。
- 函数声明:
- 由名称和对应值(函数对象(function-object))组成的一个变量对象的属性被创建;
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性。
- 变量声明:
- 由名称和对应值(undefined)组成的一个变量对象的属性被创建;
- 如果变量名称跟已经声明的形式参数或函数相同,则忽略变量声明,不干扰已经存在的这类属性。
function foo(a) {
let b = 2;
function c() {}
let d = function() {};
b = 3;
}
foo(1);
2
3
4
5
6
7
执行foo(1),进入foo的执行上下文,创建阶段,此时的AO是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
2
3
4
5
6
7
8
9
10
代码执行阶段,顺序执行代码,根据代码,修改变量对象的值,代码执行完后,这时候的AO是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
2
3
4
5
6
7
8
9
10
作用域链 #
作用域链,是由当前环境与上层环境的一系列变量对象构成的链表,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。
函数的作用域在函数定义的时候就决定了,因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
function foo() {
function bar() {
...
}
}
2
3
4
5
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。这时候执行上下文的作用域链,我们命名为 ScopeChain
,ScopeChain = [AO].concat([[Scope]])
。
上面例子当函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
2
3
4
5
6
7
8
闭包 #
闭包由两部分组成:执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
在大多数理解中,包括许多著名的书籍,文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。
let scope = "global scope";
function checkscope(){
let scope = "local scope";
function f(){
return scope;
}
return f;
}
let foo = checkscope();
foo();
2
3
4
5
6
7
8
9
10
以上例子简要执行过程:
进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
全局执行上下文初始化
执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
checkscope 执行上下文初始化,创建变量对象、作用域链、this等
checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
f 执行上下文初始化,创建变量对象、作用域链、this等
f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
当 f 函数执行的时候,checkscope 函数上下文已经被销毁(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?因为f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
2
3
具体执行分析 #
let scope = "global scope";
function checkscope(){
let scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
2
3
4
5
6
7
8
9
执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [ globalContext ];
1
2
3全局上下文初始化
globalContext = { VO: [global], Scope: [globalContext.VO], this: globalContext.VO }
1
2
3
4
5初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
1
2
3执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [ checkscopeContext, globalContext ];
1
2
3
4checkscope 函数执行上下文初始化:
复制函数 [[scope]] 属性创建作用域链;
用 arguments 创建活动对象;
初始化活动对象,即加入形参、函数声明、变量声明;
将活动对象压入 checkscope 作用域链顶端。同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]。
checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }
1
2
3
4
5
6
7
8
9
10
11执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
ECStack = [ fContext, checkscopeContext, globalContext ];
1
2
3
4
5f 函数执行上下文初始化:
- 复制函数 [[scope]] 属性创建作用域链;
- 用 arguments 创建活动对象;
- 初始化活动对象,即加入形参、函数声明、变量声明;
- 将活动对象压入 f 作用域链顶端。
fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined }
1
2
3
4
5
6
7
8
9f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
执行完毕,此时:
ECStack = [ globalContext ];
1
2
3