在JavaScript中,变量名(包括函数名)必须是合法的 标识符(identifiers)。当你考虑非传统意义上的字符时,比如Unicode,标识符中合法字符的严格和完整的规则就有点儿复杂。如果你仅考虑典型的ASCII字母数字的字符,那么这个规则还是很简单的。

一个标识符必须以a-zA-Z$,或_开头。它可以包含任意这些字符外加数字0-9

一般来说,变量标识符的规则也通用适用于属性名称。然而,有一些不能用作变量名,但是可以用作属性名的单词。这些单词被称为“保留字(reserved words)”,包括JS关键字(forinif,等等)和nulltruefalse

注意: 更多关于保留字的信息,参见本系列的 类型与文法 的附录A。

函数作用域

你使用var关键字声明的变量将属于当前的函数作用域,如果声明位于任何函数外部的顶层,它就属于全局作用域。

提升

无论var出现在一个作用域内部的何处,这个声明都被认为是属于整个作用域,而且在作用域的所有位置都是可以访问的。

这种行为称为 提升,比喻一个var声明在概念上 被移动 到了包含它的作用域的顶端。技术上讲,这个过程通过代码的编译方式进行解释更准确,但是我们先暂且跳过那些细节。

考虑如下代码:

  1. var a = 2;
  2. foo(); // 可以工作, 因为 `foo()` 声明被“提升”了
  3. function foo() {
  4. a = 3;
  5. console.log( a ); // 3
  6. var a; // 声明被“提升”到了 `foo()` 的顶端
  7. }
  8. console.log( a ); // 2

警告: 在一个作用域中依靠变量提升来在var声明出现之前使用一个变量是不常见的,也不是个好主意;它可能相当使人困惑。而使用被提升的函数声明要常见得多,也更为人所接受,就像我们在foo()正式声明之前就调用它一样。

嵌套的作用域

当你声明了一个变量时,它就在这个作用域内的任何地方都是可用的,包括任何下层/内部作用域。例如:

  1. function foo() {
  2. var a = 1;
  3. function bar() {
  4. var b = 2;
  5. function baz() {
  6. var c = 3;
  7. console.log( a, b, c ); // 1 2 3
  8. }
  9. baz();
  10. console.log( a, b ); // 1 2
  11. }
  12. bar();
  13. console.log( a ); // 1
  14. }
  15. foo();

注意cbar()的内部是不可用的,因为它是仅在内部的baz()作用域中被声明的,并且b因为同样的原因在foo()内是不可用的。

如果你试着在一个作用域内访问一个不可用的变量的值,你就会得到一个被抛出的ReferenceError。如果你试着为一个还没有被声明的变量赋值,那么根据“strict模式”的状态,你会要么得到一个在顶层全局作用域中创建的变量(不好!),要么得到一个错误。让我们看一下:

  1. function foo() {
  2. a = 1; // `a` 没有被正式声明
  3. }
  4. foo();
  5. a; // 1 -- 噢,自动全局变量 :(

这是一种非常差劲儿的做法。别这么干!总是给你的变量进行正式声明。

除了在函数级别为变量创建声明,ES6允许你使用let关键字声明属于个别块儿(一个{ .. })的变量。除了一些微妙的细节,作用域规则将大致上与我们刚刚看到的函数相同:

  1. function foo() {
  2. var a = 1;
  3. if (a >= 1) {
  4. let b = 2;
  5. while (b < 5) {
  6. let c = b * 2;
  7. b++;
  8. console.log( a + c );
  9. }
  10. }
  11. }
  12. foo();
  13. // 5 7 9

因为使用了let而非varb将仅属于if语句而不是整个foo()函数的作用域。相似地,c仅属于while循环。对于以更加细粒度的方式管理你的变量作用域来说,块儿作用域是非常有用的,它将使你的代码随着时间的推移更加易于维护。

注意: 关于作用域的更多信息,参见本系列的 作用域与闭包。更多关于let块儿作用域的信息,参见本系列的 ES6与未来