引子
常见问题
- JavaScript分段执行?怎么分段?
- JavaScript变量声明提升和函数声明提升具体怎么发生的?
- 访问变量的规则是怎样的?
看例子:
var a = 1;
function globalFun() {
console.log(a); // undefined
var a = 2;
console.log(a); // 2
// ----------------------------
var fun1 = function() {
console.log(3);
}
fun1(); // 3
var fun1 = function() {
console.log(4);
}
fun1(); // 4
// ----------------------------
function fun2() {
console.log(5);
}
fun2(); // 6
function fun2() {
console.log(6);
}
fun2(); // 6
}
globalFun();
执行结果已用注释标出,接下来我们看看以上代码到底发生了什么。
函数执行上下文
JavaScript引擎在解析到一个函数调用时,会为这个函数创建一个执行上下文,我们可以简单的理解函数执行上下文为执行函数逻辑前的一个准备工作,当函数执行完毕之后,执行上下文就会被销毁。
如果为每个函数调用都创建一个执行上下文,那么JavaScript引擎是怎么处理那么多的执行上下文的呢?答案是JavaScript引擎使用一个执行上下文栈来保存执行上下文的,每碰到一个函数调用,就会创建一个执行上下文,压入栈中,当函数执行完毕,就会执行pop操作。使用数组来模拟上述例子的执行上下文的过程如下
var contextStack = [globalContext];
contextStack.push(globalFunContext);
contextStack.push(fun1Context);
contextStack.pop(fun1Context);
contextStack.push(fun1Context);
contextStack.pop(fun1Context);
contextStack.push(fun2Context);
contextStack.pop(fun2Context);
contextStack.push(fun2Context);
contextStack.pop(fun2Context);
contextStack.pop(globalFunContext);
执行上下文栈栈底为全局执行上下文,直到前端页面关闭才会出栈。
变量对象
光知道了函数调用的时候JavaScript引擎会创建执行上下文好像没什么鸟用啊?不对,我们知道JavaScript分段执行是怎么分段的了,就是碰到函数调用就分一段呗。
那么JavaScript引擎在创建执行上下文的时候到底干了什么见不得人的事呢?其中一个很重要的事,就是为函数创建了一个变量对象,此变量对象会保存函数内部声明的变量和函数,此时其属性还不可以被访问;当执行函数体时,变量对象会转为活动对象(可以认为它们是同一个东西,只是不同的阶段叫法不一样),在函数内部访问这些变量和函数都是访问了这个活动对象的属性,具体的规则如下:
- 变量对象根据函数内部属性arguments初始化
- 将函数内部声明的变量作为自己的属性,注意,属性初始值是undefined,具体的值在代码执行的时候才赋值(变量声明提升)。
- 将函数内部使用函数声明的函数作为自己的属性,属性值为函数(函数声明提升)。
- 函数执行时,变量对象转为活动对象,变量的值不断变化,活动对象的属性值也随着修改。
- 全局变量对象是window。
- 函数的变量对象不能被显式访问。
console.log(a); // undefined
var a = 2;
console.log(a); // 2
例子中的第一个console.log是输出a的值,因为局部变量a声明提升了,是变量对象的属性值,初始值为undefined,所以输出的值为undefined;赋值操作之后,第二次输出的值为2。至于为什么不输出全局变量a的值下一节再说。
var fun1 = function() {
console.log(3);
}
fun1(); // 3
var fun1 = function() {
console.log(4);
}
fun1(); // 4
fun1同样是通过var关键字声明的一个变量,在变量对象里面初始值也为undefined,具体的值由赋值代码决定。所以第一次赋值之后,输出3,第二次赋值之后,输出4。
function fun2() {
console.log(5);
}
fun2(); // 6
function fun2() {
console.log(6);
}
fun2(); // 6
fun2是使用函数声明的函数,在变量对象里面的初始值为最后一次赋值操作的值(即在上面的例子中,fun2第二次声明覆盖了第一次的声明),所以两次调用fun2都是输出6。
作用域链
上一节提到函数内部访问的变量和函数都是执行上下文变量对象的属性,那么如果访问的变量不是变量对象的属性呢?去哪里找?按什么顺序找?答案是沿着函数的作用域链往底部找,直到找到为止或者链表遍历结束。
那什么是作用域链呢?作用域链是由多个执行上下文对应的变量对象构成的链表。它的生成过程如下:
- 函数在创建的时候,将父作用域链(即包含函数的外层函数的作用域链)复制到自己的内部属性scope中;
- 创建执行上下文之后,函数复制自身的scope属性生成链表,将自身变量对象压入链表的顶端;
- 全局作用域的作用域链为其自身的变量对象。
如果使用VO代表变量对象,那么以上例子的globalFun的作用域链创建过程如下:
var globalLink = [window];
globalFun.scope = globalLink;
globalFunLink = globalFun.scope;
globalFunLink.unshift(globalFunVO);
// globalFunLink = ['globalFunVO','window'];
对于上一节提出的问题:为什么console.log输出的不是全局变量a的值,而是局部变量a的值?因为全局变量a是全局变量对象window的属性,局部变量a是globalFunVO的属性。前面提到,函数访问变量时候查找的顺序是沿着作用域链往下找,其在自身的变量对象里面的就找到了该属性,所以不会继续往下找。
函数创建和调用的全过程
- 函数创建,保存父作用域链到函数内部属性[[scope]]
- 创建函数执行上下文,函数执行上下文压入栈中
- 函数复制自身的[[scope]]属性创建自身的作用域链
- 函数用arguments创建变量对象(活动对象),并加入变量声明、函数声明
- 将变量对象压入函数作用域链顶端
- 执行函数,修改变量对象的值