JS引擎执行代码过程可分为如下几个步骤:
- 初始化全局对象(编译阶段)
- 运行代码(执行阶段)
- 创建全局执行上下文
- 全局执行上下文入栈(调用栈)执行
一、初始化全局对象
首先V8引擎通过Parser将代码解析为AST树(具体过程详见上一篇文章~),在此过程中,V8引擎内部会帮我们在堆内存中创建一个全局对象:Global Object(GO)
- GO对象在所有的作用域中都可以访问
- 里面会包含浏览器或Node中的全局的内容,如:Date、Array、String、Number、setTimeout、setInterval等
- 其中还有一个window属性指向自己
二、运行代码
- 为了运行代码,V8引擎会有一个执行上下文栈(Execution Context Stack) ,也称函数调用栈、执行栈
- 在执行全局的代码块时,为了执行会创建一个全局执行上下文(Global Execution Context) ,全局执行上下文会被放入ECS(执行上下文栈)中被执行
- 全局执行上下文中维护着一个VO(Variable Object)此处指向GO
- GO中的全局变量是在代码被parser成AST过程中被加入进去的,要注意此时变量只是加入了进去但不会被赋值
2-1、示例1
2-1-1、代码示例
var num1 = 10;
console.log(num2); // undefined
var num2 = 20;
2-1-2、画图解析
- 编译阶段(初始化全局对象GO):
- 遇到全局变量num1,将num1加入到GO中,此时为undefined
- 遇到全局变量num2,将num2加入到GO中,此时为undefined
注意:此时并不会输出num2,这是编译阶段,只编译不运行
-
运行阶段(开始执行代码):
-
为了运行全局代码会创建一个GEC(全局执行上下文),其中的VO(变量对象)指向GO
-
GEC入执行栈开始执行代码:
-
首先将num1赋值为10:会通过VO找到GO中的num1并赋值为10,此时GO中的num1由undefined变为10
-
然后输出num2,注意,此时还未执行到num2的赋值语句,所以通过VO去GO中找到的num2为undefined,这就是为什么console.log(num2)的输出结果为undefined
-
最后将num2赋值为20,这时候GO中的num2才会由undefined变为20
-
-
2-2、示例2(含函数)
我们可以在函数定义前去调用函数,这是为什么呢?比如下面的代码块中所示:
2-2-1、代码示例
foo(123);
function foo(num) {
console.log(m); // undefined
var m = 10;
var n = 20;
}
2-2-2、画图解析
-
编译阶段:
- JS引擎会创建一个GO,当在全局作用域中发现了函数时,会在内存中开辟另一块空间去存储函数对象,函数对象包含两部分内容:
- 一部分是父级作用域(上述代码为全局作用域)
- 一部分是函数执行体(代码块,要执行的代码)
- JS引擎会创建一个GO,当在全局作用域中发现了函数时,会在内存中开辟另一块空间去存储函数对象,函数对象包含两部分内容:
-
执行阶段:
-
遇到函数执行时,会通过GO中函数变量对应的内存地址找到这个函数,在执行函数时,首先会为函数创建一个函数执行上下文,在其中也维护着一个VO,用于保存该函数的参数及创建的变量;此时会在内存中创建一个AO对象,VO指向AO
-
函数执行上下文入栈开始执行:
- 首先num被赋值为123
- 打印出m,于是便通过VO找到AO中保存的m,由于此时还未对m进行赋值,所以输出undefined
- 然后将m赋值为10
- 再将n赋值为20
-
函数体中代码执行完毕后弹出执行栈并销毁,所以VO被销毁了,也就没有指向AO的了,AO也就随之销毁了
-
2-3、示例3(含函数)
2-3-1、代码示例
var age = 10;
foo(123);
function foo(num) {
console.log(m); // undefined
var m = 10;
function bar() {
console.log(age); // 10
}
bar();
}
2-3-2、画图解析
-
编译阶段
- 首先创建GO,其中包含了foo变量(指向foo对象)及age变量
- 遇到了函数foo,内存中会额外开辟一个空间去存储foo对象,foo对象中包含了父级作用域(此处为全局作用域)以及函数体(执行代码)
-
执行阶段
-
全局执行上下文创建并入栈执行,首先为全局变量中的age赋值,此时GO中的age由undefined变为了10
-
然后执行foo函数:
- 为foo函数创建一个函数执行上下文及AO对象(其中包含了VO,指向创建的AO对象)
- 同时由于foo中存在函数bar,所以会额外为bar开辟一个空间去存储bar函数对象(bar函数对象中同样包含了父级作用域:此处为foo函数作用域、函数体)
- foo函数执行上下文入栈执行,首先num被赋值为123
- 然后输出m,由于此时m还未被赋值,且在当前作用域中发现了m,所以直接输出了undefined
- m被赋值为10,此时函数foo的AO中的m变为10
-
执行bar函数:
- 同样为bar函数创建一个执行上下文与其对应的AO对象
- bar函数执行上下文入栈执行,输出age
- 会首先去当前作用域中去找age,发现没有,那么就去其父级作用域foo中去找,发现foo的AO中也没有age,那么就接着沿着父级作用域去找,foo的父级作用域(全局作用域)中发现了age,所以最后输出10(foo函数的父级作用域为全局作用域,bar函数的父级作用域为foo函数作用域,这样形成了一条作用域链)
-
bar函数中内容执行完毕,弹出栈销毁,那么其对应的AO也会被销毁;紧接着foo执行上下文栈弹出销毁,其对应的AO也会被销毁
-
2-4、示例4(含函数,易错)
2-4-1、代码示例
var msg = 'hello word!';
function foo() {
console.log(msg);// hello word!
}
function bar() {
var msg = 'HI';
foo();
}
bar();
2-4-2、画图解析
- 编译阶段
JS引擎会创建GO对象,将全局变量msg放入,遇到了函数foo与bar,会分别开辟空间去存储foo对象与bar对象,注意,foo对象与bar对象的父级作用域都是全局作用域(GO)
- 所以,父级作用域在编译阶段就已经确定好了
-
执行阶段
-
首先GEC创建并入栈执行,会为msg赋值为hello word!
-
bar函数执行,会首先创建好bar函数的执行上下文及AO对象,其AO对象中保存着其作用域中的变量msg、foo函数变量。bar函数的执行上下文入栈执行:
- 首先会为其作用域中的msg赋值为HI
- 然后执行foo函数
-
foo函数执行,会创建好foo函数的执行上下文及其AO对象,只不过其AO对象中没有内容
- 输出msg,由于其作用域中没有msg,那么就去其父级作用域中去寻找(即GO中),在GO中找到了msg,所以输出hello word!
- 要注意,foo的函数的父级作用域在编译创建GO对象时就已经确定了!因为创建GO对象时,在全局作用域中发现了foo,所以为foo函数开辟了一个空间去存储foo对象,此时foo对象中的父级作用域信息就为GO对象了
-
foo函数执行完出栈销毁并销毁其AO对象,紧接着bar函数执行上下文出栈并销毁其AO对象不会被指向了也会随之销毁
-
三、相关面试题
3-1、题目一
var n = 100;
function foo() {
n = 200;
}
foo();
console.log(n);
解析:
-
编译时:GO对象中会保存n,foo变量,并创建foo函数对应的对象(其父级作用域为GO)
-
执行时:
-
全局执行上下文入栈执行,把n赋值为100,紧接着foo执行上下文被创建并创建其对应的AO入栈执行
-
将n赋值为200时,由于其AO中没有n,那么会向其父级作用域中寻找,也就是GO中寻找,发现了n,则把GO中的n赋值为了200
-
foo执行完毕出栈销毁
-
输出n,由于此时GO中的n被赋值为了200,所以输出200
-
3-2、题目二
function foo() {
console.log(n);
var n = 200;
console.log(n);
}
var n = 100;
foo();
解析:
- 编译时:GO对象中会保存foo变量及n,并创建foo函数对应的对象(其父级作用域为GO)
- 执行时:
- 全局执行上下文入栈执行,把n赋值为100,紧接着foo执行上下文被创建并创建其对应的AO(其AO中保存着n变量,此时还为undefined)
- foo执行上下文入栈执行,首先输出n,会先从当前作用域AO中去找,发现有,但要注意此时还未给其作用域下的n赋值,所以会先输出undefined,然后为n赋值为200,紧接着再输出n,由于n已经被赋值为200了,所以会打印出200
- foo函数执行完毕,其执行上下文出栈销毁,其对应的AO就失去了指向,也会随之销毁
3-3、题目三
var a = 100;
function foo() {
console.log(a);
return;
var a = 100;
}
foo();
解析:
- 编译时:GO对象中会保存a变量及foo变量,并创建foo函数对应的对象(其父级作用域为GO)
- 执行时:
- 全局执行上下文入栈执行,把a赋值为100,紧接着foo执行上下文被创建并创建其对应的AO(其AO中保存着a变量,此时还为undefined)
- foo执行上下文入栈执行,首先输出a,会先从当前作用域AO中去找,发现有,所以会输出undefined,然后直接return了(虽然有return,但不影响编译阶段将a放入foo的AO中)
- foo函数执行完毕,其执行上下文出栈销毁,其对应的AO就失去了指向,也会随之销毁
3-4、题目四
function foo() {
var a = b = 10;
}
foo();
console.log(a);
console.log(b);
var a = b = 0 会被转成两行代码:
var a = 0;
b = 0
b = 0 这种情况就比较特殊了,前面没有var,会被加入到全局作用域GO中,所以:
- 编译时:GO对象中会保存foo变量及b变量,并创建foo函数对应的对象(其父级作用域为GO)
- 执行时:
- 全局执行上下文入栈执行,紧接着创建foo函数的执行上下文并创建其对应的AO,AO中保存着变量b,此时为undefined
- foo执行上下文入栈执行,首先将a赋值为0,然后将全局作用域中的b赋值为0
- foo函数执行完毕,其执行上下文出栈销毁,其对应的AO就失去了指向,也会随之销毁
- 紧接着输出a,回去当前作用域去找,由于当前作用域(GO)中没有,然而这是全局作用域,它没有父级作用域的。找不到a就会报"a is not undefined"的错误,代码停止执行,所以b也就没有输出结果。
3-5、题目五
var n = 100;
function foo1() {
console.log(n);
}
function foo2() {
var n = 200;
console.log(n);
foo1();
}
foo2();
console.log(n);
-
编译时:GO对象中会保存n变量、foo1变量、foo2变量,并创建foo1函数对应的对象(其父级作用域为GO)及foo2函数对应的对象(其父级作用域为GO)
-
执行时:
- 全局执行上下文入栈执行,将GO中的n变量赋值为100
- foo2函数执行上下文及其对应的AO创建(AO中有n变量),并入栈执行
- foo2执行时将其AO中的变量n赋值为200,紧接着输出,所以先输出200
- foo1函数执行上下文及其对应的AO被创建并入栈执行
- foo1执行时输出n,由于其AO中不存在n,则去其父级作用域GO中寻找,找到了父级作用域中的n为100所以输出100
- foo1执行完毕出栈销毁并销毁其AO,紧接着foo2执行完毕出栈销毁并销毁其AO,最后输出n,首先会去当前作用域去寻找n,找到了当前作用域(全局作用域)中的n为100,所以最后输出100