块级作用域
块级作用域绑定
传统上,Javascript变量声明机制一直令我们感到困惑。在大多数类C语言在声明变量的同时也会创建变量(绑定)是在声明发生的地方创建的。然而,在JavaScript中,情况并非如此。变量的实际创建位置取决于声明它们的方式,ECMAScript6提供了使控制范围更容易的方法。
var 声明及变量提升(Hoisting)机制
在函数作用域或全局作用域中通过关键字 var
声明的变量,无论实际上是在哪里声明的,都会被当成当前作用域顶部声明的变量,这就是我们常说的提升(Hoisting)机制。下面以一个函数为例来说明:
function getValue(condition) {
if (condition) {
var value = "blue";
// 其他代码
return value;
} else {
// 此处可访问变量value,其值为undefined
return null;
}
// 此处可访问变量value,其值为undefined
}
如果您不熟悉JavaScript,可能会认为只有当 condition
的值为true时才会创建变量 value
。事实上,不管怎样都会创建变量 value
。在编译阶段,JavaScript引擎将上面的 getValue
函数更改为如下所示:
function getValue(condition) {
var value;
if (condition) {
value = "blue";
// 其他代码
return value;
} else {
return null;
}
}
变量 value
的声明被提升至函数顶部,而初始化操作依旧留在原处执行,这就意味着在 else
子句中也可以访问到该变量,且由于此时变量尚未初始化,所以其值为 undefined
。
刚接触JavaScript的开发者通常会花一些时间习惯变量提升,有时还会因误解而导致程序忠出现bug。为此, ECMAScript6引入块级作用域来强化对变量生命周期的控制。
块级声明
块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域(亦被称为词法作用域)存在于:
- 函数内部
- 块中(字符
{
和}
之间的区域)
很多类C语言都有块级作用域,而ECMAScript6引入块级作用域就是为了让JavaScriptg更灵活也更普适。
let 声明
let
声明语法与 var
的语法相同。基本上可以将 var
替换为 let
来声明一个变量,但将变量的作用域限制为只有当前的代码块(后面会讨论了其他一些细微的差异)。由于 let
声明不会被提升,因此通常会将 let
声明放在封闭代码的顶部,以便整个代码块都可以访问。这里有一个例子:
function getValue(condition) {
if (condition) {
let value = "blue";
// 其他代码
return value;
} else {
// 变量value此处不存在
return null;
}
// 变量value此处不存在
}
现在这个 getValue
函数的运行结果更像类C语言。变量 value
改由关键字 let
进行声明后,不再被提升至函数顶部。执行流离开 if
块,value
立即被销毁。如果 condition
的值为false,就永远不会声明并初始化 value
。
禁止重复声明
作用域中已经存在某个标识符,此时再使用 let
关键字声明它就会报错,举例来说:
var count = 30;
// 抛出语法错误
let count = 40;
如果当前作用域内嵌另一个作用域,是可以在内嵌作用域中使用 let
声明同名变量,示例:
var count = 30;
if (condition) {
// 不会抛出错误
let count = 40;
}
此处的let是在if块内声明了新变量count,因此不会抛出错误。内部块中的count会屏蔽全局作用域中的count,后者只有if块中才能访问到。
const 声明
使用 const
声明的是常量,其值一旦被设定后不可更改。因此,每个通过 const
声明的常量都必须初始化,示例如下:
const maxItems = 30;
// 语法报错,常量未初始化
const name;
const 与 let
const
与 let
声明的都是块级标识符,所以常量也只在当前代码块内有效,一旦执行到块外就会立即销毁。常量同样也不会被提升至作用域顶部,示例如下:
if (condition) {
const maxItems = 5;
}
// 此处无法访问maxItems
无论是使用var,还是let声明的标识符,在同一个作用域用const声明同名标识符也会抛出语法错误。举例来说:
var message = 'Hello!'
let age = 25;
// 这两天语句都会抛出错误
const message = 'Goodbye';
const age = 30;
const
声明与 let
声明有一个很大的不同,不可以为 const 定义的常量赋值,否则会抛出错误,例如:
const maxItems = 5;
// 抛出语法错误
maxItems = 5;
用 const 声明对象
const
声明不允许修改绑定,但运行修改值。也就是说使用 const
声明对象后,可以修改该对象的属性值。举个例子:
const person = {
name: 'Nicholas'
}
person.name = 'Greg';
// 抛出语法错误
person = {
name: 'Greg'
}
临时死区
与 var
不同,let
和 const
声明的变量不会被提升至作用域顶部,如果在声明之前访问这些变量,即使是使用相对安全的 typeof
操作符也会触发引用报错,举个例子:
if (condition) {
console.log(typeof value); // 引用报错
let value = 'blue';
}
由于 console.log(typeof value)
语句报错,因此后面的 let
声明变量 value
不会执行。此时的 value
还位于Javascript所谓的临时死区。
但在 let
声明的作用域外对改变量使用 typeof
则不会报错,举个例子:
console.log(typeof value); // undefined
if (condition) {
let value = 'blue';
}
循环中的块作用域绑定
经常在 for
循环中使用 var
i
声明变量
for (var i = 0; i < 10; i++) {
process(items[i])
}
console.log(i); // 10
使用 let
声明变量就会有不同的结果:
for (let i = 0; i < 10; i++) {
process(items[i])
}
console.log(i); // 此处i不可访问,抛出一个错误
循环中的函数
var
声明在循环中创建函数比较困难,因为变量到了循环外就不能访问了。例如:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function( ) {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // 输出10次数字10
});
为了解决这个问题,一般都是在循环中使用立即调用函数表达式(IIFE
),就像这样:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function( ) {
console.log(value);
}
}(i)));
}
funcs.forEach(function(func) {
func(); // 输出0,然后1、2,直到9
});
在循环内部,IIFE
表达式为接受每一个变量 i
创建了一个副本并存储了变量 value。这个变量的值就是相应迭代创建的函数所使用的值。
循环中的 let 声明
let
声明模仿上述示例中的 IIFE
所作的一切来简化循环过程,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。举个例子:
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function( ) {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // 输出0,然后1、2,直到9
});
对于 for-in
循环和 for-of
循环来说也是一样的,示例如下:
var funcs = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funcs.push(function( ) {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 输出a、b和c
});
循环中的 const 声明
对于普通的 for
循环来说,在初始化变量时使用 const
,但是 i++
更改这个变量的值就会抛出错误,就像这样:
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function( ) {
console.log(i);
});
}
如果后续循环不会修改该变量,是可以使用 const
声明的。如下示例:
var funs = [],
object = {
a: true,
b: true,
c: true
};
// 不会产生错误
for (const key in object) {
funcs.push(function( ) {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 输出a、b和c
});
全局作用域绑定
当 var 被用于全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。这意味着用 var 很可能无意中覆盖一个已经存在的全局属性,就像这样:
// 在浏览器中
var RegExp = 'Hello!';
console.log(window.RegExp); // "Hello!"
var ncz = 'Hi!';
console.log(window.ncz); // "Hi!"
在全局作用域中使用 let
或 const
,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。就是说,用 let
和 const
不会覆盖全局变量,而只能遮蔽它。示例如下:
// 在浏览器中
let RegExp = 'Hello!';
console.log(Rx); // "Hello!"
console.log((window.RegExp === RegExp); // false
const ncz = 'Hi!';
console.log(ncz); // "Hi!"
console.log((window.ncz === ncz); // false
如果希望在全集对象下定义变量,仍然可以用 var。这种情况常见于在浏览器中跨 frame
或跨 window
访问代码。