块作用域
函数作用域是最常见的作用域单元,但其他类型的作用域单元也是存在的。
除 JavaScript 外
的很多编程语言都支持块作用域
,因此对于主要使用 JavaScript 的开发者来说, 块作用域这个概念会很陌生
尽管你可能连一行带有块作用域风格的代码都没有写过, 但下面代码一定很熟悉:
for (var i=0; i<10; i++) {
console.log( i );
}
我们在 for 循环的头部直接定义了变量 i, 通常是因为只想在 for 循环内部的上下文中使用 i
, 而忽略了 i 会被绑定在外部作用域(函数或全局) 中
的事实
而这👆就是块作用域的用处。 【变量的声明应该距离使用的地方越近越好, 并最大限度地本地化】
例子:
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something( bar );
console.log( bar );
}
【bar 变量仅在 if 声明的上下文中使用, 因此如果能将它声明在 if 块内部中会是一个很有意义的事情】 但是
, 当使用 var 声明变量时
, 它写在哪里都是一样
的, 因为它们最终都会属于外部作用域
。 【这段代码是为了风格更易读而伪装
出的形式上的块作用域】, 如果使用这种形式, 要确保没在作用域其他地方意外地使用 bar 只能依靠自觉性
块作用域是一个用来对之前的最小暴露原则进行扩展的工具, 将代码从在函数中隐藏信息扩展为在块中隐藏信息
在看下面代码:
for (var i=0; i<10; i++) {
console.log( i );
}
为什么要把一个只在
for 循环内部使用(至少是应该只在内部使用) 的变量 i 污染
到整个函数作用域中呢?
但可惜,
表面上
看 JavaScript 并没有块作用域的相关功能,除深入研究
一、with
with
关键字不仅是一个难于理解的结构, 同时也是块作用域的一个例子(块作用域的一种形式
), 【用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效】
二、try/catch
JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域
, 其中声明的变量仅在 catch 内部有效
例子:👇
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
err 仅存在 catch 分句内部, 当试图从别处引用它时会抛出错误(目前此行为被修复,但静态检查工具会发出警告)
三、let
【ES6 改变了现状
, 引入了新的 let 关键字
, 提供了除 var 以外的另一种变量声明方式
】
let 关键字可以将变量绑定到所在的任意作用域中(通常是 { … } 内部)。 换句话说, let为其声明的变量隐式地了所在的块作用域
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
用 let 将变量附加在一个已经存在的块作用域上的行为是隐式的
👇
在开发和修改代码的过程中, 如果没有密切关注哪些块作用域中有绑定的变量, 并且习惯性地移动这些块或者将其包含在其他的块中, 就会导致代码变得混乱!!
为块作用域显式地创建块
可以部分解决这个问题
, 使变量的附属关系变得更加清晰。显式的块作用域风格和其他语言中块作用域的工作原理一致:👇
var foo = true;
if (foo) {
{ // <-- 显式的快
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
只要声明是有效的, 在声明中的任意位置都可以使用 { … } 括号来为 let 创建一个用于绑定的块。 在这个例子中, 我们在 if 声明内部显式地创建了一个块
提升是指声明会被视为存在于其所出现的作用域的整个范围内
【但是使用 let 进行的声明不会在块作用域中进行提升
。 声明的代码被运行之前, 声明并不“存在”】
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
let循环
一个 let 可以发挥优势的典型例子就是之前讨论的 for 循环👇
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
【for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中, 事实上它将其重新绑定
到了循环的每一个迭代中, 确保使用上一个循环迭代结束时的值重新进行赋值】
四、const
ES6 还引入了 const
, 同样可以用来创建块作用域变量
, 但其值是固定的
(常量)。 之后任何试图修改
值的操作都会引起错误
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量
a = 3; // 正常 !
b = 4; // 错误 !
}
console.log( a ); // 3
console.log( b ); // ReferenceError!