JavaScript 作用域与声明提升

如果下面这段代码作为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);

这里,当然,浏览器会弹出“1”。这里发生了什么?虽然这看起来很奇怪、危险和令人困惑,但是实际上这是该语言一个强大的表达特性。我不知道对于这种特殊的行为是否有一个标准的名称,但是我喜欢“提升”这个术语。本文将尝试解释这种机制,但是首先让我们绕道来理解JavaScript作用域。

JavaScript 作用域

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族的其他成员具有块级作用域。当控制语句进入一个块,例如if语句,新的变量会在当前作用域内被声明,而且不会影响到外部作用域。JavaScript不是这样的。在firebug中尝试这段程序:

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具有函数作用域。这和C族语言完全不一样。块级——诸如if语句,并不会创建一个新的作用域。只有函数会创建新的作用域。

对于许多习惯使用C,c++,c#,和java的程序员来说,这是意料之外的,也是不受欢迎的。幸运的是,由于JavaScript的灵活性,有一个变通方案。如果你必须在一个函数中创建一个临时作用域,这样做:

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

这个方法实际上非常灵活,可以在需要临时作用域的任何地方使用,而不仅仅是在块语句中。但是我强烈建议你花时间真正理解和欣赏JavaScript作用域。它非常强大,是我最喜欢的语言特性之一。如果你理解了作用域,提升对你来说就更有意义了。

声明,命名和提升

在JavaScript中,名称以四种基本方式之一进入作用域:

  1. 语言定义:默认情况下,所有的作用域都给定了 thisarguments 这两个名称。
  2. 形式参数:函数可以有命名的形式参数,这些参数的作用域限定在函数的主体上。
  3. 函数声明:这些是类似函数foo(){}的形式。
  4. 变量声明:这些声明的形式是var foo;

函数声明和变量声明总是被JavaScript解释器无形地移动(提升)到其包含范围的顶部。显然,函数参数和语言定义的名称已经存在。这意味着如下代码:

function foo() {
	bar();
	var x = 1;
}

实际上被解释成这样:

function foo() {
	var x;
	bar();
	x = 1;
}

事实证明,是否执行包含声明的行并不重要。以下两个函数是等价的:

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”的命名被提升,但是主体被留在后面,在执行过程中才会被分配。

这涵盖了提升的基本知识,它并不像看上去那么复杂或令人困惑。当然,这是JavaScript,在某些特殊情况下会稍微复杂一些。

命名解析顺序

要记住的最重要的特殊情况是命名解析顺序。请记住,有四种方法可以让命名进入给定的作用域。我上面列出的顺序就是它们被解析的顺序。通常,如果已经定义了名称,则不会被同名的其他属性覆盖。这意味着函数声明优先于变量声明。这并不意味着对该名称的赋值不起作用,只是声明部分将被忽略。也有一些例外:

  1. 内置的arguments名称行为很奇怪。它似乎是按照形式参数声明的,但在函数声明之前。这意味着一个命名为arguments的形式参数的优先级高于内置的arguments,即便它没有定义。这是一个不好的特性。不要用arguments作为形参的名称。
  2. 尝试在任何地方使用名称this作为标识符将导致语法错误(SyntaxError)。这是一个很好的特性。
  3. 如果多个形式参数具有相同的名称,则列表中最后一个的参数将优先,即使该参数未定义。

命名函数表达式

可以给函数表达式中定义的函数命名,语法类似于函数声明。这并不会使它成为一个函数声明,名称也不会被纳入作用域,函数体也不会被提。这里有一些代码来说明我的意思:

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"
// 这里spam仅仅表示baz所指向的函数的名称为spam
// console.log(baz.name)  结果为 "spam"

如何用这些知识编写代码

现在您已经了解了作用域和提升,这对于JavaScript编码意味着什么呢?最重要的是始终用var语句来声明变量。我强烈建议每个作用域只有一个var语句,并且它应该位于顶部。如果你强迫自己这样做,你将永远不会有与提升相关的困惑。然而,这样做会使跟踪哪些变量实际上已经在当前作用域中被声明变得困难。我建议使用带有onevar选项的JSLint来实现这一点。如果你已经完成了所有这些,你的代码应该是这样的:

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

What the Standard Says

我发现直接参考ECMAScript标准(pdf)来理解这些东西是如何工作的通常很有用。下面是它对变量声明和作用域的说明(旧版本的12.2.2节):

如果变量语句发生在FunctionDeclaration中,则在该函数中使用function-local作用域定义变量,如10.1.3节所述。否则,他们被使用属性特性{DontDelete}定义在全局作用域中。(也就是说,它们作为全局对象的成员创建,如10.1.3节所述)(译者注:原文,Otherwise, they are defined with global scope using property attributes { DontDelete }.) 当进入执行作用域时变量被创建。一个块(Block)并不会创建新的执行作用域。只有程序和函数声明(FunctionDeclaration)才能产生新的作用域。 当变量被创建时被初始化为undefined。当变量声明语句(VariableStatement)被执行时,带有初始化器的变量才被赋予赋值表达式(AssignmentExpression)中的值,而不是变量被创建的时候。

我希望本文对JavaScript程序员最常见的困惑之一有所帮助。我已经尽可能彻底避免造成更多的混乱。如果我有任何错误或遗漏,请让我知道。

原文链接:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值