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 aa = 2两条语句,第一个是编译阶段的任务,第二个是执行阶段的任务。

带来的结果是所有在一个作用域中的声明,不管在何处,都会在代码被执行前先被处理。你可以把这个想象成声明(变量和函数)被“移动”到他们各自作用域的顶部,这个动作我们成为声明提升(hoisting)。

声明会被提升,但是赋值,包括函数表达式的赋值,都不会被提升。

小心对待重复的声明,特别是混合普通var和函数的声明。

posted on 2015-04-18 22:41  锟斤拷  阅读(259)  评论(0编辑  收藏  举报