2.2 变量

在JavaScript中,变量的名称(包括函数名称)必须是有效的标识符。考虑到Unicode这样的非传统字符的情况,标识符中有效字符的严格完整规则有点复杂。如果只是考虑常用的ASCII字母数字的话,那么规则是非常简单的。

标识符必须由a~z、A~Z、$或 开始。它可以包含前面所有这些字符以及数字0~9。

一般来说,变量标识符的这个规则对属性命名也同样适用。但是,有些单词不能用作变量名,但可以作为属性名。这些单词被称为“保留词”,其中包括JavaScript关键字(for、in、if等)以及null、true和false。

有关保留字的更多信息,参见本系列《你不知道的JavaScript(中卷)》第一部分的附录A。

函数作用域

如果使用关键字var声明一个变量,那么这个变量就属于当前的函数作用域,如果声明是发生在任何函数外的顶层声明,那么这个变量则属于全局作用域。

1. 提升

无论var出现在一个作用域中的哪个位置,这个声明都属于整个作用域,在其中到处都是可以访问的。

这一行为被比喻地称为提升(hoisting), var声明概念上“移动”到了其所在作用域的最前面。从技术上来说,可以通过如何编译代码更精确地解释这个过程,我们暂时先不赘述这些细节。

考虑:

        var a = 2;
        foo();                 // 因为foo()而运行
                            // 声明是“被提升的”
        function foo() {


            a = 3;


            console.log( a );  // 3


            var a;             // 声明是“被提升的”
                            // 到foo()的顶端
        }


        console.log( a );      // 2

在变量声明出现之前,依靠变量提升在其作用域使用这个变量并不常见,也并不是一个好的想法;这样的代码可能会令人非常迷惑。相比之下,使用提升后的函数声明则要常见得多,就像我们在foo()的正式声明出现前就调用了它。

2. 嵌套作用域

声明后的变量在这个作用域内是随处可以访问的,包括所有低层/内层的作用域。举例来说:

        function foo() {
            var a = 1;


            function bar() {
              var b = 2;


              function baz() {
                  var c = 3;


                  console.log( a, b, c );  // 1 2 3
              }


              baz();
              console.log( a, b );         // 1 2
            }


            bar();
            console.log( a );                // 1
        }


        foo();

注意,在上述示例中,c在bar()的内部是不可访问的,因为它只声明在内层baz()作用域,b在foo()中是不可访问的,也是同样的原因。

如果试图在一个作用域中访问一个不可访问的变量,那么就会抛出ReferenceError。如果试图设定尚未声明的变量,那么就会导致在顶层全局作用域创建这个变量(不好!)或者出现错误,这要根据是否处于“严格模式”而定(参见2.4节)。我们来看一下:

        function foo() {
            a = 1;  // a没有正式声明
        }


        foo();
        a;          // 1--哎呀,自动全局变量 :(

这是一个很差的实践。不要这么做!一定要正式声明你的变量。

除了在函数层级声明变量,ES6还支持通过let关键字声明属于单独块({ .. }对)的变量。除了细节上有一些微小差别,这里的作用域规则和我们在函数中看到的基本上是相同的:

        function foo() {
            var a = 1;


            if (a >= 1) {
              let b = 2;


              while (b < 5) {
                  let c = b * 2;
                  b++;


                  console.log( a + c );
              }
            }
        }


        foo();
        // 5 7 9

因为使用了let而不是var,所以b只属于if语句,不属于整个foo()函数的作用域。与此类似,c只属于while循环。块作用域非常有助于更细化地管理变量作用域,从而更容易随着时间的发展而维护代码。

有关作用域的更多信息,参见本系列中的《你不知道的JavaScript(上卷)》第一部分。有关let块作用域的更多信息,参见本书第二部分。