在上一篇博文"谈谈JavaScript的算数运算、二进制浮点数舍入误差及比较、类型转换和变量声明提前问题"中提到,函数、变量声明会提前,却没有解释为什么。本文从执行上下文和变量对象两个角度谈谈为什么函数、变量申明会提前。
关于执行上下文(Execution Contexts)和变量对象,个人认为网上有几篇比较好的博文:
1)深入理解JavaScript系列(11):执行上下文(Execution Contexts),深入理解JavaScript系列(12):变量对象(Variable Object),汤姆大叔的博文,更多是ECMA-262标准理解及翻译,词汇、术语比较专业,又比较容易理解;
2)前端基础进阶(二):执行上下文详细图解,前端基础进阶(三):变量对象详解,个人认为写的很好,读者朋友们若想深入了解执行上下文和变量对象,不妨阅读这2篇博文。
每当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。执行上下文可以理解为当前代码的运行环境,JavaScript运行环境大概包括三种情况:
因此在JavaScript代码执行过程中,必定会产生多个执行上下文,JavaScript引擎会以栈的方式来处理它们,不妨以ECStack=[]来表示:
1)首先会压入全局上下文,[globalEC]
2)函数调用,创建函数上下文,压入ECStack,[globalEC, functionEC]
3)函数调用完毕,退出函数上下文,从栈顶ECStack弹出,[globalEC]
4)全局上下文一直都存在,直到页面或浏览器关闭。
执行上下文有其生命周期:创建阶段和代码执行阶段。
看个DEMO
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
每当代码执行到test()时,就会创建新的函数执行上下文,并压入ECStatck[globalEC, testEC],testEC有2个阶段,伪代码表示如下:
// 创建过程
testEC = {
VO: {}, // 变量对象
scopeChain: {},
明确this指向
}
VO = {
arguments: {...},
foo: <foo reference> // 表示foo的地址引用
a: undefined
}
// 执行阶段
VO -> AO // VO被激活,用AO表示,Active Object
AO['a'] = 1
```js
所以demo代码,等同:
```js
function test() {
function foo() { //函数声明优先级高于变量
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}
test();
至此,我们应该明白了为什么变量、函数声明会提前啦,总结一下,2句话:
1)执行上下文有2个阶段,创建阶段(进入上下文)、执行阶段;
2)在创建阶段,会创建变量对象,变量对象包含下列属性:
同样是声明提前,但是函数声明优先级高于变量声明,函数声明先执行,如果该变量名(同名函数已经申明)则变量不会覆盖之前定义,但是后续赋值会覆盖函数声明。
看一个比较经典的demo:
alert(x); // function x() {}
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
结合上下文、变量对象,代码等同:
function x() {};
alert(x); // function x() {}
x = 10;
alert(x); // 10
x = 20;
alert(x); // 20
如果不是函数声明,而是函数表达式呢?
console.log(x); // undefined
var x = 10;
console.log(x); // 10
x = 20;
var x = function() {};
console.log(x); // function x() {}
函数表达式,等同与变量定义,根据代码先后顺序执行,上述代码等同:
var x;
console.log(x); // undefined
x = 10;
console.log(x); // 10
x = 20;
x = function() {};
console.log(x); // function x() {}