大家好,我是前端西瓜哥。今天在一个前端交流群里,一个群友发了这么一道题。
function fn(a) { console.log(a); var a = 2; function a() { } console.log(a); } fn(1);
一看就知道是变量提升题。然后我试着猜了下,果不其然做错了。var 和它的变量提升果然让人迷惑。
我们先学习一些底层的一些知识。
var
var 是 ES6 之前用来声明变量的关键字。var 这人看起来挺机灵,但是干起活来却非常离谱。
块作用域中用 var 写的变量声明,会往外跑,跑到最近的函数作用域或全局作用域中。
当然也有一些比较特殊的情况,比如 with
和 try...catch
,它们可以形成特殊的块作用域,让变量不往外跑。
一道非常经典的循环使用定时器的题目就是考察了这个特性:
for (var i = 0; i < 5; i++) { setTimeout(function(){ console.log(i); }, i * 1000); }
这题我不讲,因为太经典了,没做过的朋友可以试着做一下。
解决方案是用 IIFE(立即执行函数表达式)来形成一个 var 不会外溢的函数作用域,当然实际开发中更常见的做法是抛弃 var,使用支持块作用域的 let/const。
如果一个变量名没有被声明过,且没有使用 var 就赋予了初始值,它就会成为全局作用域下的变量。
function setA() { a = 1; } setA(); console.log(a); // 1
如果多次声明同一个变量, 之后的 var 声明会被忽略掉。
var a = 1; var a = 2; var a = 3;
等价于
var a = 1; a = 2; a = 3;
函数声明你也可以认为是一个 var 声明,千万不要以为后面的 var 声明能够覆盖掉同名的函数声明。
声明的提升
变量声明和函数声明都是会发生 提升 (hoist)的。
怎么理解,就是在执行代码前,会先扫描一下代码,将其中类似 var a = 1
的语句中声明的部分,也就是 var a
先找出来,先执行。
这是编译器的行为,所以在表现上,就是声明被放到了代码的开头,成为 提升。
除了 var / let / const 声明的变量会提升,函数声明也会提升,且函数声明会优先于变量提升,即放到变量声明的上方。
b(); var a = 1; function b() { console.log(a) };
等价于
function b() { console.log(a) }; var a; b(); // 输出 undefined a = 1;
有一个易错点,就是把函数表达式当成了函数声明。函数表达式是不会发生变量提升的,切记。
回到本题
function fn(a) { console.log(a); var a = 2; function a() { } console.log(a); } fn(1);
首先是函数的参数 a,它等价于一个外部的一个变量,上面代码等价于:
var a = 1; (function() { function a() { } console.log(a); a = 2; console.log(a); })();
这里函数声明做了提升,跑到最顶部。接下来考虑给 var a = 2
做提升。但我们的函数 a 的声明已经提升了,所以这里的 var 算是重复声明了,直接去掉 var,最终得到上述代码。
我们很容易看出答案是:
ƒ a() { } 2
本题除了考察比较常规的声明提升,还考察了一个比较特别的情况,就是 函数的参数声明应该放在哪里?
答案是放到一个额外的包裹着函数体的中间层中,而不是直接放到函数体的顶部。
说真的,谁要是在工作中这样写代码,我可能要好好问候一下他。
最后
正经前端开发在实际工作中都是使用 let / var 的,然后交给编译器转换为兼容 ES5 的使用 var 的代码,再发布到生产环境。
var 是个坏文明,总是在秀它那没有下限的骚操作,尤其是在嬉皮笑脸的面试官的操纵下。