变量声明“被提前”
JavaScript 的语法和 C 、Java、C# 类似,统称为 C 类语法。有过 C 或 Java 编程经验的同学应该对“先声明、后使用”的规则很熟悉,如果使用未经声明的变量或函数,在编译阶段就会报错。
然而,JavaScript 却能够在变量和函数被声明之前使用它们。下面我们就深入了解一下其中的玄机。
先来看一段代码:
(function() { console.log(a);//a is not defined
})();
运行上面代码立马就报错,不过,这也正是我们期望的,因为 a变量根本就没有定义过嘛!再来看看下面的代码:
(function() { console.log(a); //输出undefined var a= "hello"; console.log(a);//输出的是hello })();
首先,上面这段代码是正确的,没有任何问题。但是,为什么不报错了?a变量是在调用语句后面定义的啊?为什么居然输出的是 undefined
?
这其实是 JavaScript 解析器搞的鬼,解析器将当前作用域内声明的所有变量和函数都会放到作用域的开始处,但是,只有变量的声明被提前到作用域的开始处了,而赋值操作被保留在原处。上述代码对于解析器来说其实是如下这个样子滴:
(function() { var a; //声明被提前到作用域开始处了! console.log(a); a= "hello"; //赋值操作还在原地! console.log(a); })();
这就是为什么上述代码不报异常的原因!变量和函数经过“被提前”之后,a变量其实就被放在了调用函数的前面,根据 JavaScript 语法的定义,已声明而未被赋值的变量会被自动赋值为 undefined
,所以,第一次打印 a变量的值就是 undefined
,后面我们对a变量进行了赋值操作,所以,第二次再打印变量就会输出hello;
再来看一个例子:
var name = "aaa";
(function () {
console.log(name); //undefined
var name = "bbb";
console.log(name); //bbb
})();
上述代码中,我们先声明了一个变量 name
,我们的本意是希望在第一次打印 name
变量时能够输出全局范围内定义的 name
变量,然后再在函数中定义一个局部 name
变量覆盖全局变量,最后输出局部变量的值。可是第一次输出的结果和我们的预期完全不一致,原因就是我们定义的局部变量在其作用域内被“提前”了,也就是变成了如下形式:
var name = "aaa"; (function () { var name; //注意:name 变量被提前了! console.log(name); name = "bbb"; console.log(name); })();
由于 JavaScript 具有这样的“怪癖”,所以你会看到很多编码指南建议大家将变量声明放在作用域的最上方,这样就能时刻提醒自己注意了。
函数声明“被提前”
前边说的是变量,接下来我们说说函数。
函数的“被提前”还要分两种情况,一种是函数声明,第二种是函数作为值赋值给变量。
先说第一种情况,上代码:
isItHoisted();//输出yes function isItHoisted() { console.log("Yes!"); }
如上所示,JavaScript 解释器允许你在函数声明之前使用,也就是说,函数声明并不仅仅是函数名“被提前”了,整个函数的定义也“被提前”了!所以上述代码能够正确执行。
再来看第二种情况:函数作为值赋值给变量。(还记得吗?在 JavaScript 中,函数也可以作为值赋予变量!)还是先上代码:
aaa();//输出hello bbb();//输出 undefined is not a function function aaa() { console.log("hello"); } var bbb= function () { console.log("hello"); };
我们做了一个对比,aaa函数被妥妥的执行了,符合第一种类型;bbb变量“被提前”了,但是他的赋值(也就是函数)并没有被提前,从这一点上来说,和前面我们所讲的变量“被提前”是完全一致的,并且,由于“被提前”的变量的默认值是 undefined
,所以报的错误属于“类型不匹配”,因为 undefined
不是函数,当然不能被调用。
总结
通过上面的讲解可以总结如下:
1.变量的声明被提前到作用域顶部,赋值保留在原地
2.函数声明整个“被提前” 函数作为值赋给变量时只有变量“被提前”了,函数没有“被提前”