目录
声明提升
变量提升
两个示例
观察如下代码:
a = 1;
var a;
console.log(a);
将js代码从上往下一行一行地分析:
- 第一行声明了a全局变量,并赋值1(在非严格模式下,引擎进行LHS查询,在全局作用域中创建一个具有名称a的变量)
- 第二行再次声明变量a,将a重新默认值undefined
- 打印a
按照以上思路,最终打印的结果应该是undefined,但实际上我们打印出了1。这是因为发生了变量提升。代码的实际执行顺序如下:
var a;
a = 1;
console.log(a); // 1
再看另一段代码:
console.log(a);
var a = 1;
同样,如果将js代码从上往下一行一行地分析:
- 第一行代码发生RHS查询,对一个未声明的变量a进行取值,会抛出
ReferenceError
异常 - 第二行代码不再执行
但事实上,代码不会抛出任何错误,而是打印出undefined。这是因为出现了变量提升。代码的实际执行顺序如下:
var a;
console.log(a); // a被赋予默认值undefined
a = 1;
变量提升的位置
那么,变量是提升到哪里了呢?是当前作用域的最前方,还是全局作用域的最前方?
用var声明变量时(下面示例以此为前提,ES6中的let完美地解决了以下问题),当其所在作用域为块作用域时,var声明的变量将属于外部作用域(污染了外部作用域)。而当其所在作用域为函数作用域时,外部作用域将无法访问包装函数内部的任何内容,此时,var声明的变量就成功地成为了局部变量。
看如下代码:
{
a = 1;
var a;
console.log("作用域内",a);
}
console.log("作用域外",a);
a的声明在块作用域中,但它实际上属于外部作用域。再根据之前谈到的变量提升。两个console.log
打印的a都为1。此时,变量被提升到了全局作用域,也就是污染了全局作用域。
(function f(){
a = 1;
var a;
console.log("作用域内",a);
})(); // 函数立即执行
console.log("作用域外",a);
a的声明在函数作用域中,a成为了该函数作用域内的局部变量。因此在外部作用域调用时将抛出ReferenceError
错误,而在函数内部正常输出1。
由此可见,将变量封装在函数内部可以避免变量污染全局。如果想让代码正常执行,可以采用函数自调用。
函数提升
函数声明会被提升,函数表达式不会被提升。首先简单地了解一下什么是函数声明和函数表达式。
函数声明和函数表达式辨析
// 函数声明
function f() {
console.log("函数声明");
}
// 函数表达式
var a = function() {
console.log("函数表达式");
}
函数表达式就是将函数赋值给某一变量。该函数可以是匿名函数也可以是具名函数(例子中为匿名函数)。
函数声明的提升
f();
function f() {
console.log(1);
}
在该代码中,函数声明提升到函数调用之前。函数调用正常执行,打印出1。
函数表达式不会出现提升
- 匿名函数表达式
f();
var f = function() {
console.log(1);
}
抛出TypeError
错误,代码可以被理解为:
var f; // f为默认值undefined
f(); // 对undefined进行调用抛出TypeError错误
f = function() {
console.log(1);
}
- 具名函数表达式
f();
var f = function g() {
console.log(1);
};
与上面代码一样,抛出TypeError
错误
g();
var f = function g() {
console.log(1);
};
可以理解为:
var f;
g(); // ReferenceError
f = function g() {
console.log(1);
};
g未被声明,抛出ReferenceError
错误。
函数提升优先
当变量提升和函数提升同时存在时,会优先函数提升。
如下这段代码:
console.log(foo);
function foo() {
console.log(1);
}
foo = 2;
会打印出
ƒ foo() {
console.log(1);
}
对函数冲突和变量冲突的处理
对于冲突的函数声明,后面的声明会覆盖前面的声明。
对于冲突的变量声明,后面的声明会被忽略。
其实,对于这个很好理解,函数声明时是有函数体的,因此设置后面的内容优先是很合理的。而变量声明时,变量并没有被赋值(初始化),因此再多的变量声明也都是一样。
下面,来简单测试一下:
变量冲突:
consle.log(f);
function f(){
console.log(1);
}
var f = 1;
首先遇到函数声明,然后遇到变量声明(被忽略),打印出的依然是函数f。
ƒ f(){
console.log(1);
}
函数冲突:
console.log(f);
var f = 1;
function f(){
console.log(1);
}
首先遇到变量声明,然后遇到函数声明(覆盖变量声明),依然打印出函数f。
ƒ f(){
console.log(1);
}
这个机制也很好的反应出了上面提到的函数提升优先的特性。
为什么会出现声明提升?
我们知道,引擎在解释JavaScript代码之前会先进行编译。编译过程中会找到所有的声明,并用合适的作用域将他们关联起来。也就是说,所有声明都会在代码被执行之前首先被处理。因此,声明发生在编译阶段,赋值发生在执行阶段。所以在代码执行时,所有的声明已经结束,也就是出现了声明提升。
有var和没有var的区别
由以上例子,我们可以发现,有var时声明提升正常进行。那没有var时,又会发生什么呢?
观察如下代码:
a = 1;
console.log(a);
正常打印出1。当给a前面加上var时,也正常打印出1。依据LHS查询,引擎会在全局作用域中创建一个具有名称a的变量。在本示例中,代码就在全局作用域中,因此,加不加var输出结果都一样。
再看这段涉及变量提升的代码:
console.log(a);
a = 1;
当有var时,输出undefined。当没有var时,抛出ReferenceError
错误。可见在这里没有出现变量提升,也就是说全局变量a并没有被创建。因为代码在编译时,没有找到a的声明,在执行阶段中,console.log(a)
发生RHS查询,找不到变量a。自然抛出ReferenceError错误。由此可见,a = 1
创建一个具有名称a的全局变量这个过程是在执行阶段发生的。当没有执行到这一步时,就没有变量a。
所以规范的代码书写(将变量清晰地声明在相关作用域,避免未声明就使用和作用域污染)将有利于代码编译,从而提高代码执行效率。