在JavaScript中,变量名(包括函数名)必须是合法的 标识符(identifiers)。当你考虑非传统意义上的字符时,比如Unicode,标识符中合法字符的严格和完整的规则就有点儿复杂。如果你仅考虑典型的ASCII字母数字的字符,那么这个规则还是很简单的。
一个标识符必须以a
-z
,A
-Z
,$
,或_
开头。它可以包含任意这些字符外加数字0
-9
。
一般来说,变量标识符的规则也通用适用于属性名称。然而,有一些不能用作变量名,但是可以用作属性名的单词。这些单词被称为“保留字(reserved words)”,包括JS关键字(for
,in
,if
,等等)和null
,true
和false
。
注意: 更多关于保留字的信息,参见本系列的 类型与文法 的附录A。
函数作用域
你使用var
关键字声明的变量将属于当前的函数作用域,如果声明位于任何函数外部的顶层,它就属于全局作用域。
提升
无论var
出现在一个作用域内部的何处,这个声明都被认为是属于整个作用域,而且在作用域的所有位置都是可以访问的。
这种行为称为 提升,比喻一个var
声明在概念上 被移动 到了包含它的作用域的顶端。技术上讲,这个过程通过代码的编译方式进行解释更准确,但是我们先暂且跳过那些细节。
考虑如下代码:
var a = 2;
foo(); // 可以工作, 因为 `foo()` 声明被“提升”了
function foo() {
a = 3;
console.log( a ); // 3
var a; // 声明被“提升”到了 `foo()` 的顶端
}
console.log( a ); // 2
警告: 在一个作用域中依靠变量提升来在var
声明出现之前使用一个变量是不常见的,也不是个好主意;它可能相当使人困惑。而使用被提升的函数声明要常见得多,也更为人所接受,就像我们在foo()
正式声明之前就调用它一样。
嵌套的作用域
当你声明了一个变量时,它就在这个作用域内的任何地方都是可用的,包括任何下层/内部作用域。例如:
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
。如果你试着为一个还没有被声明的变量赋值,那么根据“strict模式”的状态,你会要么得到一个在顶层全局作用域中创建的变量(不好!),要么得到一个错误。让我们看一下:
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
循环。对于以更加细粒度的方式管理你的变量作用域来说,块儿作用域是非常有用的,它将使你的代码随着时间的推移更加易于维护。
注意: 关于作用域的更多信息,参见本系列的 作用域与闭包。更多关于let
块儿作用域的信息,参见本系列的 ES6与未来。