学习笔记——JavaScript执行过程
第一步,解析代码
当V8引擎开始解析JavaScript代码时,其内部会在堆内存中创建一个全局对象(GlobalObject也称为go),里面会包含String,Array,Number,setTimeOut等等一些全局对象和全局函数。这就是为什么能直接在代码中使用String、Math、Date这些类,或者使用setTimeout、setInterval这些函数的原因了。
var num1 = 10;
var num2 = 20;
var result = num1 - num2;
// 伪代码示意
var globalObject = {
String: "类",
Date: "类",
setTimeount: "函数",
// 指向自己的Window属性
window: globalObject,
// 解析时会将这些属性定义到go中,但是此时代码还未执行,所以值都是undefined
num1: undefined,
num2: undefined,
result: undefined,
};
第二步,运行代码
1. Execution Context Stack
v8引擎为了执行代码, 内部存在一个执行上下文栈(Execution Context Stack, 简称ECStack)(函数调用栈),是一个执行代码的调用栈。ECStack是一个栈结构,js代码执行函数时,就会把这个函数压入栈中,执行完便会将这个函数弹出栈。
函数调用栈.png2. Global Execution Context
- ECStack一般是用于执行函数的,但是上面要执行的是全局代码, 所以创建了全局执行上下文(Global Execution Context)(全局代码需要被执行时才会创建),就是构建一个全局代码块放入ECStack中执行。
- GEC有两部分组成,一个用来维护一个VO(variable Object),它指向GO对象;另一个用来执行代码(自上往下依次执行),如图中先执行
var num1=10
,就会通过VO找到GO里的num1:undefined
,然后将undefined改成10 -
console.log(window)
打印的就是go对象,执行时会通过AO找到GO里的window属性,这个属性又是指向go自身的。
执行GEC.png
函数执行
执行函数时,在函数代码的前面就可以执行函数。这是因为在解析函数时,js引擎会为函数开辟一块内存空间来存储它,这块空间会存放函数父级作用域和函数的执行体,下面的bar函数的执行体就是var b = 10; console.log("bar");
,父级作用域是globalObject也就是window。
bar()
function bar(a) {
var b = 10
console.log("bar");
}
// 伪代码示意
var globalObject = {
String: "类",
Date: "类",
setTimeount: "函数",
window: globalObject,
// 这里保存的是bar函数的地址
bar: 0x100
};
- 解析时先生成了一个Go对象,发现里面有个bar函数,就会在内存开辟一块空间用于存放bar函数的父级作用域和执行体,然后把这个地址复制给bar属性。
- 解析完后开始执行代码,执行bar时,通过AO找到Go里的bar,发现这个bar是个内存地址,就会根据这个地址找到为bar函数创建的空间。然后继续执行(),括号是执行函数的意思,此时js引擎会创建一个函数执行上下文(Function Execution Context,简称FEC)用来存放bar函数空间里的内容,然后将FEC放入ECStack中执行。
- FEC中也有一个VO,指向的是AO对象(自动创建的),用于定义函数内部的参数,变量等等(未赋值),开始执行函数体时才会进行赋值等操作。执行完后这个FEC就会弹出栈并销毁,AO失去指向也会被销毁。如果后面的代码又执行了bar函数,就会重新创建FEC和AO。
作用域链
- 当我们查找一个变量时,真实的查找路径是沿着作用域链来查找。FEC中的
scope chain:VO+ParentScope
就是作用域链,由当前VO对象和父级作用域组成,这里bar函数的父级作用域就是全局对象了(在函数编译时就被确定了),就是GEC中的GO对象。函数的嵌套也是同样的道理,顺着作用域链层层向上查找。 - 如下面的代码在bar函数里打印name,就会顺着scope chain,先从当前函数的VO开始找,此时的VO指向AO,发现AO中没有,会去找父级作用域,就是一开始的全局执行上下文(GEC),GEC的作用域就是GEC的VO,也就是GEC的GO,发现里面有abc,就会使用GEC的abc变量。
var name = 'lj'
bar()
function bar(a) {
var b = 10
console.log(name);
}
作用域链.png
练练手
var message = "Hello Global"
function foo() {
console.log(message)
}
function bar() {
var message = "Hello Bar"
foo()
}
bar()
这是一个典型的作用域链问题,最终打印的结果是“Hello Global”,因为在foo函数定义时,他的父级作用域是全局作用域,也就是GO,foo函数的作用域并不受调用位置的影响,而是和定义的位置有关系的。
var a=10
function fun(){
console.log(a)
return
var a = 20
}
fun()
这里会打印“undefined”,因为定义时,fun函数的VO是有a:undefined
的,return只是不执行赋值的操作。
理解
无论是执行全局变量,还是函数,都会进入ECStack中执行,全局变量被包裹到全局执行上下文(GEC)里进入ECStack中执行,而函数会被包裹到函数执行上下文(FEC)里进入ECStack中执行。这两个上下文都会在内部创建一个VO对象,分别用于指向GEC的GO和FEC的AO,这里的AO是执行函数时创建的对象,用于存放函数内的参数、变量等属性。执行代码时,用到的变量会按照作用域链进行查找,先从本身的AO或GO中寻找,如果没有则会去父级作用域中查找,就是父级的AO或者GO。
文章的内容都是基于早期的ECMA的版本规范,就是es5之前的规范。es5之后的版本,将VO改成了VE(Variable Environment 变量环境),在执行代码中变量和函数的声明会作为环境记录添加到变量环境VE中,但形式不严格要求成对象,可以是map、list或者对象等等,基本的逻辑是差不多的。