JavaScript 块作用域
块作用域
是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。
【之前的代码】:
for(var loop = 0; loop < 10; loop++) {
console.log(loop);
}
【说明】:我们在 for 循环的头部直接定义了变量 loop,通常是因为只想在 for 循环内部的上下文中使用 loop,而忽略了 loop 实际上会被绑定在所处的作用域中。如果能够将 loop 声明在 for 循环内部会是一个很有意义的事情,而现在只能依靠自觉性来防止自己没在作用域其他地方意外地使用 loop 变量。也正是基于这个原因,ES6 推出了块作用域的概念。
【块作用域的用处】:变量的声明应该距离使用的地方越近越好,并最大限度地本地化。
with
with 可以创建一个词法作用域,将对象的属性作为声明在当前作用域的变量(函数)。换言之,with 从对象中创建的作用域仅在 with 声明中而非外部作用域中有效。不可不推荐使用 with 来实现块作用域,在这里提出只是为了说明 with 有这个功能。
try/catch
JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。
【注意】:其中声明的变量是指 catch(variable),括号内的变量,而不是在 {} 中声明的变量。
【示例】:
try {
undefined();
} catch(err) {
var name = "spirit";
console.log(err);
}
console.log(name); // spirit
console.log(err);
let
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 创建一个用于绑定的块。
【注意】:使用 let 进行的声明不会在块作用域中进行提升。声明的代码被执行之前,声明并不存在。
{
console.log(bar); // ReferenceError!
let bar = 2;
}
1. 垃圾收集
另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。
【示例】:
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = {
// ...
};
process(someReallyBigData);
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
}, false);
【问题】:click 函数的点击回调并不需要 someReallyBigData 变量。理论上这意味着当 process() 执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于 click 函数形成了一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现)。
【示例】:
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容完事可以销毁!
{
let someReallyBigData = {
// ...
};
process(someReallyBigData);
}
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
}, false);
【解释】:使用块作用域可以让引擎清楚地知道没有必要继续保存 someReallyBigData 。
【建议】:为变量显式声明块作用域,并对变量进行本地化绑定是非常有用的工具,可以把它添加到你的代码工具箱中了。
2. let 循环
【示例】:
for(let loop = 0; loop < 10; loop++) {
console.log(loop);
}
console.log(loop); // ReferenceError
// 等价于
{
let j;
for(j = 0; j < 10; j++) {
let i = j; // 每个迭代重新绑定!
console.log(i);
}
}
【解释】:for 循环头部的 let 不仅将 loop 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
【注意】:由于 let 声明附属于一个新的作用域而不是当前的函数作用域(也不属于全局作用域),当代码中存在对于函数作用域中 var 声明的隐式依赖时,就会有很多隐藏的陷阱,如果用 let 来替代 var 则需要在代码重构的过程中付出额外的精力。
const
除了 let 以外,ES6 还引入了 const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何视图修改值的操作都会引起错误。
小结
- 块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 {} 内部)。
- 从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。
- 在 ES6 中引入了 let 关键字,用来在任意代码块中声明变量。
- 有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。
附录:块作用域的替代方案
从 ES3 发布以来,JavaScript 中就有了块作用域,而 with 和 catch 分句就是块作用域的两个小例子。
【示例】:
{
let a = 2;
console.log(a); // 2
}
console.log(a); // ReferenceError
【问】:如何在 ES6 之前也可以实现上述示例的效果。
【答】:使用 catch。
try { throw 2; } catch(a) {
console.log(a); // 2
}
console.log(a); // ReferenceError
【解释】:利用 catch 分句具有块作用域来实现,除了丑陋且奇怪了一些,还是挺好用的。
性能
- 首先,try/catch 的性能的确很糟糕,但技术层面上没有合理的理由来说明 try/catch 必须这么慢,或者会一直慢下去。自从 TC39 支持在 ES6 的转换器中使用 try/catch 后,Traceur 团队已经要求 Chrome 对 try/catch 的性能进行改进,他们显然有很充分的动机来做这件事情。
- 其次,IIFE 和 try/catch 并不是完全等价的,因为如果将一段代码中的任意一部分拿出来用函数进行包裹,会改变这段代码的含义,其中的 this、return、break 和 contine 都会发生变化。IIFE 并不是一个普适的解决方案,它只适合在某些情况下进行手动操作。
- 最后问题就变成了:你是否想要块作用域?如果你想要,这些工具就可以帮助你。如果不想要,继续使用 var 来写代码就好了。