JavaScript作用域与声明提升【翻译】

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

JavaScript作用域与声明提升

你知道下面JavaScript执行后alert的值吗?

1 var foo = 1;
2 function bar() {
3     if (!foo) {
4         var foo = 10;
5     }
6     alert(foo);
7 }
8 bar();

如果因为结果为“10”而吃惊,那么可能你需要好好看看这篇文章:

1 var a = 1;
2 function b() {
3     a = 10;
4     return;
5     function a() {}
6 }
7 b();
8 alert(a);

这个呢?浏览器会alert为“1”,那么这些结果为何如此呢?确实这些看起来有点奇怪,危险,并且混乱,但这恰恰是说明JavaScript是一个强大和富有表现力的语言。我不知道这种特性的标准名称,但是我习惯叫它"提升“,这篇文章将尝试阐明这一机制,但首先我们需要先了解JavaScript的作用域。

JavaScript的作用域

许多初学者最容易混淆的就是“作用域”,实际上,它不只初学者容易混淆,我见过很多有经验的Javascript程序员也不能完全理解“作用域”。之所以弄不清JavaScript的作用域,是因为把它理解成了类C语言的作用域。思考下面的C程序:

 1 #include <stdio.h>
 2 int main() {
 3     int x = 1;
 4     printf("%d, ", x); // 1
 5     if (1) {
 6         int x = 2;
 7         printf("%d, ", x); // 2
 8     }
 9     printf("%d\n", x); // 1
10 }

这个程序的输出结果为1,2,1.这是因为C,和类C语言有块级作用域。当代码执行到一个块内,例如一对大括号内,新的变量声明在这个作用域内,不会影响大括号的外部。这是不同于JavaScript的。在Firebug下尝试以下代码:

1 var x = 1;
2 console.log(x); // 1
3 if (true) {
4     var x = 2;
5     console.log(x); // 2
6 }
7 console.log(x); // 2

在这种情况中,Firebug会显示1,2,2.这是因为JavaScript为函数作用域,这完全不同于类C语言的块级,比如在大括号内,它是不会创建新的作用域的。只有在函数才会。

而很多语言都使用的块级作用域,比如C,C++,C#,和Java,所以这很容易让刚学JavaScript的程序员无法理解,幸好,JavaScript的函数定义非常灵活,如果逆需要在一个函数内创建一个临时的作用域,你可以这样:

 1 function foo() {
 2     var x = 1;
 3     if (x) {
 4         (function () {
 5             var x = 2;
 6             // some other code
 7         }());
 8     }
 9     // x is still 1.
10 }

声明变量和提升

在JavaScript中,一个变量进入作用域有四种基本途径:

  1. 语言定义:全局作用域,默认情况下,有变量this和arguments。
  2. 形参:函数可以有形参,它的作用域为整个函数内。
  3. 函数声明:例如这种形式 function foo() {}
  4. 变量声明:例如这种形式 var foo;

函数声明和变量声明会在解析JavaScript程序是进行内部“提升”,形参和全局变量已经存在了,意味着提升的是3,4两种变量类型,意味着代码在解析后会像这样:

 1 function foo() {
 2     bar();
 3     var x = 1;
 4 }
 5 实际上解释后会像这样: 6 
 7 function foo() {
 8     var x;
 9     bar();
10     x = 1;
11 }

事实证明,不管是否包含变量声明它都是存在的。下面两个函数是等价的:

 1 function foo() {
 2     if (false) {
 3         var x = 1;
 4     }
 5     return;
 6     var y = 1;
 7 }
 8 function foo() {
 9     var x, y;
10     if (false) {
11         x = 1;
12     }
13     return;
14     y = 1;
15 }

注意:当函数作为变量定义的值时声明不会被提升,这种情况只有变量名会被提升,这导致函数名提升了,但函数体没有被提升,但请记住函数声明有两种形式,考虑以下JavaScript:

 1 function test() {
 2     foo(); // TypeError "foo is not a function"
 3     bar(); // "this will run!"
 4     var foo = function () { // function expression assigned to local variable 'foo'
 5         alert("this won't run!");
 6     }
 7     function bar() { // function declaration, given the name 'bar'
 8         alert("this will run!");
 9     }
10 }
11 test();

在这个例子中,只有包含函数体的函数声明会被提升到顶部,而变量"foo"被提升了,但是它的主体在右边,只有在语句执行到此时才会被分配。

这就是提升的基本概念,这样看起来也不是那么复杂和容易混淆了吧。当然在写JavaScript时,会遇到一些特殊情况会稍微复杂点。

变量名解析顺序

最重要的是记住在特殊情况下的变量名解析顺序。它们有四种方式进入命名空间,这个顺序根据列表从上到下一次进行,这个顺序列表在下面列出,在一般情况下,如果一个命名已经被定义,那么它不会被另一个同名的所覆盖,这就意味着一个函数声明要优先于变量声明,这并不等于分配新的命名无效,只是声明将被屏蔽。他们也有些例外:

  • 内置arguments的怪异情况,它看起来在函数声明之前形参已经被声明,这意味着一个形参名arguments将优先于内置的arguments,即使它没有被定义,这是一个不好的特征。不要使用arguments作为一个形参。
  • 如果定义this这个命名在一些地方,会导致语法错误。这是一个好的特性。
  • 如果多个形参有相同的名字,那么形参中最后一个同名的将被优先,哪怕它没有被定义。

函数命名表达式

你可以使用函数表达式的形式将函数定义赋给一个函数名,语法像函数定义,但它不同于函数声明,这个命名没有进入作用域,函数体也没有提升。这里有一些代码作为例子来说明其含义:

 1 foo(); // TypeError "foo is not a function"
 2 bar(); // valid
 3 baz(); // TypeError "baz is not a function"
 4 spam(); // ReferenceError "spam is not defined"
 5 
 6 var foo = function () {}; // anonymous function expression ('foo' gets hoisted)
 7 function bar() {}; // function declaration ('bar' and the function body get hoisted)
 8 var baz = function spam() {}; // named function expression (only 'baz' gets hoisted)
 9 
10 foo(); // valid
11 bar(); // valid
12 baz(); // valid
13 spam(); // ReferenceError "spam is not defined"

如何使用这个知识来写代码

现在你了解了作用域与命名提升,但如何写JavaScript代码呢?一个非常重要的事情是在任何声明变量的时候都使用var.我强烈建议你在每一个作用域内的头部使用var定义变量,如果你总是如此,你将不会因为提升的特性而混乱。然而,这样做很难区分当前作用域下实际被声明的变量和以有的变量。我建议使用JSLint的onevar选项来执行这些。如果你准备这样做,那么你的代码应该看起来是这样:

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

规范怎么说明的

我发现经常查阅ECMAScript Starndard去理解这些特性是如何工作的是非常有用的。这里是它说明变量声明和作用域(最新版的12.2.2部分)

如果变量声明在函数声明内部,那么这个变量被定义在当前函数作用域内,由10.1.3节所述。另外,他们都被定义在全局作用域(即,他们创建的是全局对象的成员,由10.1.3所述)的属性上,并具有属性的特性。变量被创建在当前执行的作用域内,一个块没法产生一个新的作用空间,只有程序和函数声明产生新的作用空间。变量被初始化时创建成一个undefined。一个变量真正被初始化是在使用表达式给变量分配一个存在的值。不是在变量被创建时。

我希望这篇文章解释清楚了JavaScript代码中的一些易混淆的特性。我试图全面的解释这些问题,避免产生新的问题,如果产生了更多的混淆,如果我翻了任何错误或重大遗漏,请让我知道。

posted @ 2015-08-09 15:45  羊肉烧卖  阅读(589)  评论(0编辑  收藏  举报