you don't know js -- Scope and Closures学习笔记——第四章(声明提升 Hoisting)
以下内容为自己看原版尝试做的翻译,仅当一个自己的看书记录,书中内容绝大部分都翻译了,但由于个人能力有限,建议各位看客不要迷信翻译的质量,推荐购买其英文原版学习观看。
现在,一想到作用域以及变量声明的位置和方式决定其所属的作用域层,你应该感到很舒适。函数作用域和块级作用域遵守相同的规则,在这个方面:在一个作用域内部声明的任何变量属于这个作用域。
先有鸡还是先有蛋
有一种倾向认为在一个JavaScript程序中你所看到的所有代码都是一行接一行从上到下顺序执行的。虽然大体上这是正确的,但这种说法中的一部分会带来错误的思维。
考虑如下代码:
a = 2;
var a;
console.log(a);
你认为在console.log(..)
语句中会打印什么?
许多代码猴都认为是undefined
,因为var a
语句在a = 2
的后面,这看起来会很自然的假定变量被重定义了,并且为其赋了默认的undefined
。然而,输出的结果将会是2
。
考虑另外一段代码:
console.log(a);
var a = 2;
你可能倾向于假设,以为前一个代码段表现出了less-than-top-down的行为,可能在这里,同样也会打印2
。另一些人可能认为因为变量a
在其声明前被使用,这将会抛出一个ReferenceError
。
不幸的是,上面两种猜测都是错的,输出的结果会是undefined
。
那么,咋回事儿捏?似乎我们遇到了一个先有鸡还是先有蛋的问题。哪一个先出来,声明(“蛋”)?还是赋值(“鸡”)?
编译器再现江湖
为了回答这个问题,我们需要重提第一章关于编译器的讨论。回想下引擎会在解释你的JS代码前先进行编译。编译阶段的一部分工作就是找到并关联对应作用域中的变量。第二章告诉我们这是词法作用域的核心。
所以,最好的思考方式是所有的声明,包括变量和函数,都会在代码执行前优先处理。
当你看到var a = 2;
时,你可能认为他是一条语句。但是JavaScript实际上将它看做两条语句: var a;
和 a = 2;
第一条语句(声明语句),在编译阶段被处理。第二条语句(赋值语句)留在原地等待执行阶段。
我们的第一个代码段应该考虑被处理成如下样子:
var a;
a = 2;
console.log(a);
第一部分是声明,第二部分是执行。
同样的,第二个代码段被处理成:
var a;
console.log(a);
a = 2;
所以,可以把这个处理过程想象成变量和函数的声明被从他们定义的地方“移动”到这个代码流的顶端。这种行为有一个名字叫hoisting。
换句话说,蛋(声明)在鸡(赋值)的前面。
Only the declarations themselves are hoisted, while any assignments
or other executable logic are left in place. If hoisting
were to re-arrange the executable logic of our code, that
could wreak havoc.
foo();
function foo(){
cosole.log(a); // undefined
var a = 2;
}
函数foo
的声明被提升了,因此在第一行对函数的调用可以被执行。
同时需要注意的是变量提升是per-scope的。因此上面的函数更准确的说会被解释成下面的样子:
function foo(){
var a;
console.log(a); // undefined
a = 2;
}
foo();
正如我们所见的,函数声明被提升了。但是函数表达式并不会被提升。
foo(); // not ReferenceError, but TypeError!
var foo = function bar(){
// ...
};
变量foo
被提升并附加到这个程序的global作用域,因此foo()
并不会抛出RefferenceError
。但是foo
此时还没有值(除非他是函数定义式而不是函数表达式)。所以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而不是2!这个代码段会被引擎解释为:
function foo(){
console.log(1);
}
foo(); // 1
foo = function(){
console.log(2);
};
注意var foo
是一个重复的声明(因此被忽略),尽管它出现在function foo()...
声明的前面,因为函数声明被提升在普通变量的前面。
虽然多重/重复的var
声明被有效的忽略,连续的function
声明却会覆盖前面的。
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版本中会改变,所以最好避免在块中声明函数。
复习
我们忍不住会将var a = 2;
看做一个语句,但JavaScript引擎不这么看。它会把其看成var a
和a = 2
两条语句,第一个是编译阶段的任务,第二个是执行阶段的任务。
带来的结果是所有在一个作用域中的声明,不管在何处,都会在代码被执行前先被处理。你可以把这个想象成声明(变量和函数)被“移动”到他们各自作用域的顶部,这个动作我们成为声明提升(hoisting)。
声明会被提升,但是赋值,包括函数表达式的赋值,都不会被提升。
小心对待重复的声明,特别是混合普通var
和函数的声明。