写在前面
在开始这篇文章之前,你需要提前了解一些概念:JavaScript的词法作用域、常见的嵌套作用域类比于气泡模型、产生一个气泡有函数和块儿两种方式。
这篇文章介绍什么时候需要使用块儿作用域,即使用块儿作用域的价值。其中创建块儿作用域的方法主要是let
和const
,本文使用let
创建块儿作用域。
价值
首先,我们来看一个代码片段:
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something( bar );
console.log( bar );
}
复制代码
我们可以理解这个代码片段的意图,当foo
为true
的情况,去声明变量bar
,再对bar
做数据操作。但是不幸的是,实际上无论foo
为true
还是false
,变量bar
都会被声明,因为提升(Hoisting)。 这就造成变量bar
去污染整个作用域(因为是多余的)。
这是块儿作用域可以做的第一个价值——治污染,即去除多余的变量声明。仅仅需要将var bar
的var
更改为let
:
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
复制代码
bar
将仅存在于if(..){...}
块儿作用域内。
我们再来看第二个代码片段:
function process(data) {
// 做些有趣的事
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
复制代码
前半部分是拿process
函数处理someReallyBigData
这个数据,后半部分是按钮的事件绑定。但是前半部分和后半部分是毫无关联的,所以从性能上考虑,我们是希望在process
函数执行之后,someReallyBigData
这个消耗巨大内存的数据结构可以被垃圾回收。然后JS引擎很可能仍会保持这个结构一段时间,因为click
函数在整个作用域上拥有一个闭包。
这是块儿作用域可以做的第二个价值——垃圾回收。适当修改一下前面这个代码片段:
function process(data) {
// 做些有趣的事
}
// 运行过后,任何定义在这个块中的东西都可以消失了
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
复制代码
我们再来看第三个代码片段,一个常见的面试题目:
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
复制代码
我们可以理解这个代码片段想要输出的是1,2,3,4,5,但其实输出的是6,6,6,6,6。因为在5次timer
函数执行的时候,由于闭包找到的相同的作用域内i
的值已经变成了6。
想要输出1,2,3,4,5,有个方便的方法是为每次的timer
绑定一自个儿的块儿作用域,这是块儿作用域的第三个价值——循环中的块儿作用域。代码修改为 :
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
复制代码
也可以是:
{
let j;
for (j=1; j<=5; j++) {
let i = j;
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
}
复制代码
工具与更好的使用
在前面我们使用let
创建块儿作用域,但是let
/const
都是ES6引入的,如果在ES6之前,想要使用块儿作用域,就使用try...catch的catch块儿。
Google 维护着一个称为“Traceur”[^note-traceur]的项目,它的任务正是为了广泛使用 ES6 特性而将它转译为前 ES6(大多数是 ES5,但不是全部!)代码。TC39 协会依赖这个工具(和其他的工具)来测试他们所规定的特性的语义。
比方这样的代码片段:
{
let a = 2;
console.log( a ); // 2
}
console.log( a ); // ReferenceError
复制代码
Traceur会转换成:
{
try {
throw undefined;
} catch (a) {
a = 2;
console.log( a );
}
}
console.log( a );
复制代码
另一个是在使用let创建块儿作用域的时候,创建更加明确的块儿(考虑到代码移动、代码复用、可读性等)。比如我们第一个治污染的例子:
var foo = true;
if (foo) {
/*let*/{
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
复制代码
YDKJS的作者Kyle Simpson做了个名为let-er的工具,let-er 是一个编译期代码转译器,它唯一的任务就是找到 let 语句形式并转译它们。它允许这么写let语句:
var foo = true;
if (foo) {
let(bar = foo*2){
bar = something( bar );
console.log( bar )
}
}
console.log( bar ); // ReferenceErr
复制代码
总结与参考链接
用let
还是var
是一个考虑是否使用块儿作用域的问题。如果决定使用块儿作用域,创建更加明确的块儿是推荐的。另外摘了冴羽 对于let
和const
的最佳实践:
在我们开发的时候,可能认为应该默认使用
let
而不是var
,这种情况下,对于需要写保护的变量要使用const
。然而另一种做法日益普及:默认使用const
,只有当确实需要改变变量的值的时候才使用let
。这是因为大部分的变量的值在初始化后不应再改变,而预料之外的变量值的改变是很多 bug 的源头。
参考链接: