JavaScript在V8引擎中执行过程
非函数执行过程
var num1 = 20;
var num2 = 30;
var result = num1 + num2;
console.log(result);
- 代码进行解析,V8引擎内部在解析成
AST语法树之前会帮助我们创建一个GlobalObject(GO)对象,内容如下:
var GlobalObject = {
String: "字符串类",
Date:"日期类",
Number,
setTimeout,
num1: undefined,
num2: undefined,
result: undefined,
window: GlobalObject
}
编译阶段所有变量都会进入预编译阶段,没有赋值
- 运行代码
- V8为了执行代码,内部会有一个执行上下文栈(函数调用栈:
Execution Context Stack, ECStack) - ECStack一般用于执行函数,因为我们执行的全局代码中没有函数,因为我们的全局代码中需要创建全局作用域上下文(
Global Execution Context,GEC) - 全局作用域包含:
Variable Object:GO
- V8为了执行代码,内部会有一个执行上下文栈(函数调用栈:
准备工作完成,开始执行代码,开始给
GlobalObject内部属性进行赋值,num1 = 20,num2 = 30,result = 50,打印50,GO会在解析阶段存放所有的变量,并且初始值为undefined,代码执行开始进行相关的赋值,得到真正的结果,上述GO解析过程其实就是变量提升.
函数执行过程
var name = '123';
//执行:foo() => foo
function foo (){
console.log('foo');
}
编译阶段:
var GlobalObject = {
String: "字符串类",
Date:"日期类",
Number,
setTimeout,
name: undefined,
foo: undefined,
window: GlobalObject
}
编译阶段如果发现是函数,会在ECStack重新创建一个内存空间用于存放函数对象,函数对象包含了两个属性:
[[sope]]:parent scope- 函数的执行体 在原来堆空间该函数属性存放了函数对象的地址,属性元素指向该地址。
foo(123);
function foo(num){
console.log(m);
var m = 10;
var n = 20;
console.log('foo');
}
foo(321);
上述代码执行过程:
foo在GO空间存储的依然是内存地址,该地址指向了一个新开辟的函数存储空间,遇到执行时期在ECStack调用栈内生成一块FEC,FEC指向了新开辟的地(VO环境),传值为123时,AO空间内num就有了赋值,此时undefined还处于提升阶段结果为undefined,函数内部开始执行,执行赋值打印结束以后,函数外部发现没有引用该函数的地址,所以函数就会被销毁。
如果再次发生调用,在ECStack内部会重新开辟一块创建FEC对象,重复上述操作,直到函数空间被销毁为止.
作用域链
var name = 'why';
foo(123);
function foo(num){
console.log(m);
var m = 10;
var n = 20;
console.log(name);
}
查找变量的过程其实是沿着作用域链进行查找的。
当前作用域链
scope chain包含了两块内容:VO和ParentScope两块节点,name变量在执行的过程中发现AO环境没有定义该变量,会去上层作用域ParentScope去进行查找,而ParentSocpe则是在函数创建的时候就已经指向了GlobalObject,所以会引用上层父级作用域的var name = "why"
var name = 'why'
foo(123);
function foo(num){
function bar(){
console.log(name)
}
}
foo();
内存空间执行过程:
函数执行完成以后发现没有引用就会从栈内弹出来然后删除掉。
作用域链查找过程:
- 现在自己的
AO空间进行查找 - 如果
AO空间没有就会去上层作用域GO空间进行查找 - 如果上层作用域
GO空间也没有就会再去上上层空间查找直至找到为止 - 如果最外层空间都没有找到变量的赋值就会报错:
Uncaguht ReferenceError: xx is not defined建议慎用name属性,可能会导致name属性本身存在于window对象上
全局作用域分析
var message = "Hello Global";
function foo (){
console.log(message);
}
function bar (){
var message = "Hello bar";
foo();
}
foo();
易混淆的点是:函数在编译阶段就已经确定父级作用域,不是在执行阶段才知道.查找变量阶段不是查找运行处的变量,而是去父级作用域进行查找.
变量环境和记录
EO和AO的规范只适用于早期的ECMA(5)规范,在最新的ECMA规范中,每一个执行上下文会关联到一个变量环境(VariableEnviroment)作为环境记录添加到变量环境中,对于函数来说,形参也会被作为环境记录添加到变量环境中.
作用域面试题
题目一:
var n = 100;
function foo(){
n = 200;
}
foo();
console.log(n);
题目二:
function foo(){
console.log(n); //undefined
var n = 200;
console.log(n); //200
}
var n = 100;
foo();
解析:
- 首先会创建一个
GlobalObject对象,里面存储了foo的地址,n的变量(undefined) GO对象开始赋值,n赋值为100,foo函数依然保存地址- 函数
foo新开辟一个空间AO,开始执行代码,执行函数从上到下执行 - 打印
n,发现在自己的内部空间AO已经提前解析了n变量,结果是undefiend,第二次打印n结果就是上面定义的200
题目三:
var n = 100;
function foo1(){
console.log(n)
}
function foo2(){
var n = 200;
console.log(n);
foo1();
}
foo2();
console.log(n);
解析:
- 首先在堆内存开辟一个空间
GlobalObject用于存储上述变量,n是undefined,foo1,foo2均是将要新开辟的内存地址 - 新开辟foo1,foo2内存空间,进行变量解析,
n是undefined - 代码进入执行阶段
- foo2内部的
n会先查找自己内部的AO空间是否有n变量的赋值,查找发现有:n=200 - foo1内部打印n,发现是否n变量,发现没有,然后去自己内存上层空间查找,上层空间
GlobalObject是有的,赋值为n=100 - 最后打印n,此时的n是全局变量
n=100
题目四:
var a = 100;
function foo(){
console.log(a);
return;
var a = 100;
}
与上面比较类似的是,虽然return后面的代码不会进行执行,但是在编译阶段依然会给a进行赋值为undefined,对于foo这个函数来说内部的空间已经有了赋值的变量a,就不会去上层作用域进行查找,因为结果就是undefined.
补充:
function other(){
m = 200; //意思默认JS引擎对m进行了一个处理,将其添加到全局变量`GlobalObject`中去了
}
题目五:
function foo(){
var a = b = 100;
}
foo();
console.log(a);
console.log(b);
/*
* 等价于 var a = 100; b=100;
*/
结果打印:
在AO空间内部是有a的变量定义的undefined,但是在GO空间内a是not defined,b是100