你不知道的JS之作用域和闭包(四)(声明)提升
原文:你不知道的js系列
先有鸡还是先有蛋?
如下代码:
a = 2; var a; console.log( a );
很多开发者可能会认为结果会输出 undefined,因为 var a 在 a = 2 后面,好像变量似乎被重新定义了,所以结果会是默认值 undefined。
然而,正确结果是 2.
下面这段代码,a 在 定义之前被引用。不会抛出错误,也不会输出 2 。
console.log( a ); var a = 2;
结果输出 undefined。
编译器
回忆一下第(一)节中的内容,引擎在解释执行之前会编译你的代码。
所有的声明,包括函数和变量,都会在代码执行之前被处理。
你看到 var a = 2;是一个语句,实际上 JavaScript 会认为这是两个语句,第一个声明 var a 在编译期间被处理,第二个语句赋值操作,留在原处等待执行。
所以上面的第一段代码会被处理成两部分
var a;
a = 2;
console.log( a );
第二段代码实际上也被处理成:
var a;
console.log( a );
a = 2;
这个过程,可以看成是变量和函数的声明被提升到代码的头部,叫做变量提升(Hoisting)
也就是说变量声明在赋值语句之前被执行。
注:只有声明被提升,赋值和其它的操作被留在原处。
foo(); function foo() { console.log( a ); // undefined var a = 2; }
上面代码中的函数 foo 的声明被提前,所以这样的调用是可以被执行的。
提升发生在每个作用域内部,所有变量 a 的声明被提升到 foo 的顶部。相当于:
function foo() { var a; console.log( a ); // undefined a = 2; } foo();
函数声明被提升了,但是函数表达式不会被提升:
foo(); // not ReferenceError, but TypeError! var foo = function bar() { // ... };
这里的变量 foo 的声明被提升了,所以对 foo 的 RHS 查询不会报错。但这个时候 foo 的值还是 undefined,所有会报 TypeError 错误。
就算是有命名的函数表达式,这个名称标识也不会被外部作用域访问:
foo(); // TypeError bar(); // ReferenceError var foo = function bar() { // ... };
上面这段代码会被解释为类似下面的:
var foo; foo(); // TypeError bar(); // ReferenceError foo = function() { var bar = ...self... // ... }
函数优先
函数和变量的声明都会被提升,但是一个细微的区别就是函数声明在前面。
考虑下面的代码:
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
输出结果为1
因为这段代码会被解释为:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
var foo 是一个重复的声明,所有被忽略掉了。即时它在函数声明的前面编写,因为函数声明的提升要先于变量声明的提升。
和函数名称重复的变量声明就会被忽略,但如果有多个同名的函数声明,那么后面的声明会覆盖掉前面的:
foo(); // 3 function foo() { console.log( 1 ); } var foo = function() { console.log( 2 ); }; function foo() { console.log( 3 ); }
在普通的代码块中出现的函数声明也会被提升到外部作用域的顶部。
foo(); // "b" var a = true; if (a) { function foo() { console.log( "a" ); } } else { function foo() { console.log( "b" ); } }
但这种行为并不靠谱,因为有可能将来 JavaScript 的版本会改变这个机制(比如在块中声明的函数将会绑定在块作用域中),所以要避免在块中声明函数。
小结:
变量声明提升,函数声明提升,其它操作不变
函数表达式不会被提升
函数声明优先于变量声明
后声明的会覆盖先声明的