[翻译]JavaScript Scoping and Hoisting

原文连接html

你知道下面这段JavaScript代码段执行出来的结果吗?程序员

var foo = 1;
function bar() {
	if (!foo) {
		var foo = 10;
	}
	alert(foo);
}
bar();
复制代码

若是结果“10”令你惊讶,那么下面这个程序真的会让你找不着北。面试

var a = 1;
function b() {
	a = 10;
	return;
	function a() {}
}
b();
alert(a);
复制代码

浏览器会alert("1")。那么,到底发生了什么?这看起来可能很陌生、很古怪而且使人困惑,但这正是这个语言强而有力的表现特征。我不清楚这个特征的专有名词,可是我更愿意用“hoisting”来表达。这篇文章将试着去揭开这种机制的面纱,可是咱们先着重理解JavaScript的做用域。express

Scoping in JavaScipt

对于JavaScript初学者来讲,做用域是产生困惑的根源之一。事实上,不只包括初学者,我遇到的不少有经验的JavaScript程序员都没有充分理解做用域。在JavaScript的做用域上有如此之多的困惑的根源是由于它看起来很像C系的语言。思考下面的C程序:浏览器

#include <stdio.h>
int main() {
	int x = 1;
	printf("%d, ", x); // 1
	if (1) {
		int x = 2;
		printf("%d, ", x); // 2
	}
	printf("%d\n", x); // 1
}
复制代码

这个程序的输出是1, 2, 1。这是由于C和其余C系的语言都有块级做用域(block-level scope)当流程控制走进了块级域,例以下面的if语句块,能够在这个做用域声明一个新的变量,而不影响外部的做用域。这不一样于JavaScript。在Firebug里面试试下面的代码:bash

var x = 1;
console.log(x); // 1
if (true) {
	var x = 2;
	console.log(x); // 2
}
console.log(x); // 2
复制代码

这种状况下,Firebug会打印1, 2, 2。这是由于JavaScript有函数做用域(function-level scope)。这彻底不一样与C系语言的块级做用域,例以下面的if语句块里面不会建立一个新的做用域。只有function才能建立新的做用域。ide

对于许多熟悉像C,C++,C#或者Java的程序员来,这个设定是超出预期而且不友好的。幸运的是,鉴于JavaScript中函数的灵活性,有个曲线救国的方法。若是你必定要在function中建立临时的做用域,能够作以下尝试:函数

function foo() {
	var x = 1;
	if (x) {
		(function () {
			var x = 2;
			// some other code
		}());
	}
	// x is still 1.
}
复制代码

这个方法确实很灵活,而且能在任什么时候候须要临时做用域时使用,不只仅局限在块语句中。可是,我强烈的建议你真的须要应该花些时间去理解和正确的认识JavaScript的做用域。它真的颇有用,也是这个语言吸引个人特点之一。若是你理解了做用域,hoisting对你来讲也将变得容易许多。ui

Declarations, Names, and Hoisting

在JavaScript,以一个名称存在于做用域有如下4中方法:this

1.语言自己定义(Language-defined):全部做用域默认包含this和arguments。

2.形式参数(Formal parameters):函数能带入形式参数,使其能从函数外部做用域进入函数内部做用域。

3.函数声明(Function declarations):这是函数声明的形式 function foo(){}。

4.变量声明(Variable declarations):声明的形式 var foo;

函数的声明和变量的声明老是被JavaScript编译器偷偷的提高(“hoisted”)到它们所在做用域的顶部。函数参数和语言自己定义的已经明显的存在在那里。这种形式像下面这段代码:

function foo() {
	bar();
	var x = 1;
}
复制代码

事实上被编译成下面这样:

function foo() {
	var x;
	bar();
	x = 1;
}
复制代码

结论是声明的那行是否被执行都是可有可无的。下面两个function是等价的:

function foo() {
	if (false) {
		var x = 1;
	}
	return;
	var y = 1;
}
function foo() {
	var x, y;
	if (false) {
		x = 1;
	}
	return;
	y = 1;
}
复制代码

须要注意的是,分配赋值的部分没有被提高。仅仅是命名的部分被提高。这与函数声明不一样,整个函数体也会被提高。可是请记住有两种常规的办法能够声明函数。参考下面的JavaScript代码:

function test() {
	foo(); // TypeError "foo is not a function"
	bar(); // "this will run!"
	var foo = function () { // function expression assigned to local variable 'foo'
		alert("this won't run!");
	}
	function bar() { // function declaration, given the name 'bar'
		alert("this will run!");
	}
}
test();
复制代码

这种状况下,只有函数声明的形式才会带着函数体一块儿提高。函数表达式形式:“foo”被提高了,可是它的函数体部分被遗留在赋值的时候执行。

以上涵盖了基本的提高(“hoisting”),并非看起来那么复杂和使人迷惑。固然,做为JavaScript,在特殊状况下是会有那么一些复杂的东西。

Name Resolution Order

在大多数重要特殊的时候应时刻铭记在心是名称解析的顺序。牢记一个名称进入做用域有四种方法。我在上面列举的例子就是他们解析的顺序。总的来讲,若是一个名称已经被定义,它永远不会被另外一个同名的不一样属性覆盖。这意味着函数的声明要优先于变量的声明。但这并不表明对着名称的复制不起做用,仅仅只是声明部分被忽略。这里有一些例外:

  • 内置的arguments有些古怪,它彷佛在形式参数以后声明,可是有是在函数声明以前。这一位置形式参数比arguments拥有更高的优先级,即便这个参数是undefined。这是个坏的特性。不要使用arguments做为形式参数。
  • 试图使用this做为会形成SyntaxError错误。这是个好的特性。
  • 若是有多和形式参数名称如出一辙,对优先使用最后一个,及时这个参数是undefined。

Named Function Expressions

你能够在函数表达式中给函数定义名称,就像函数声明的语句同样。这样并不能使它成为函数的声明,而且这个名称没有被带入到做用域,函数体也没有被提高。下面是一些代码来阐明个人意思:

foo(); // TypeError "foo is not a function"
bar(); // valid
baz(); // TypeError "baz is not a function"
spam(); // ReferenceError "spam is not defined"

var foo = function () {}; // anonymous function expression ('foo' gets hoisted)
function bar() {}; // function declaration ('bar' and the function body get hoisted)
var baz = function spam() {}; // named function expression (only 'baz' gets hoisted)

foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"
复制代码

How to Code With This Knowledge

如今你理解了做用域和变量提高,可是对编写JavaScript来讲意味着什么呢?最重要的是,声明你的变量的时候老是使用var语句。我强烈建议你在每一个做用域的首位使用var语句。若是你强制本身这样作,你将永远不会被提高的问题困扰。然而作这个会使追踪当前做用域实际声明了哪些变量变得困难。我建议在JSLint中设置onevar选项来控制这个。若是你已经我说的全部工做,那么你的代码有点像下面这样:

/*jslint onevar: true [...] */
function foo(a, b, c) {
    var x = 1,
    	bar,
    	baz = "something";
}
复制代码

What the Standard Says

我发现直接去查询ECMAScript Standard(PDF)理解这些东西是如何运做的方式是最有用的。这是我讨论的关于变量神经和做用域的段落(section 12.2.2 in the older version):

If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local scope in that function, as described in section 10.1.3. Otherwise, they are defined with global scope (that is, they are created as members of the global object, as described in section 10.1.3) using property attributes { DontDelete }. Variables are created when the execution scope is entered. A Block does not define a new execution scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.

我但愿这篇文章能帮助到许多那些有着共同困惑的JavaScript程序员。我已经很努力尽量的直接的阐述,避免制造更多的困惑。若是我写错了或者遗漏了什么,请让我知道。

相关文章
相关标签/搜索