你不懂JS:作用域与闭包 附录B:填补块儿作用域
感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大奖:点击这里领取
在第三章中,我们探索了块儿作用域。我们看到最早在ES3中引入的with
和catch
子句都是存在于JavaScript中的块儿作用域的小例子。
但是ES6引入的let
最终使我们的代码有了完整的,不受约束的块作用域能力。不论是在功能上还是在代码风格上,块作用域都使许多激动人心的事情成为可能。
但要是我们想在前ES6环境中使用块儿作用域呢?
考虑这段代码:
{
let a = 2;
console.log( a ); // 2
}
console.log( a ); // ReferenceError
它在ES6环境下工作的非常好。但是我们能在前ES6中这么做吗?catch
就是答案。
try{throw 2}catch(a){
console.log( a ); // 2
}
console.log( a ); // ReferenceError
哇!这真是看起来丑陋和奇怪的代码。我们看到一个try/catch
似乎强制抛出一个错误,但是这个它抛出的“错误”只是一个值2
。然后接收它的变量声明是在catch(a)
子句中。三观:毁尽。
没错,catch
子句拥有块儿作用域,这意味着它可以被用于在前ES6环境中填补块儿作用域。
“但是……”,你说。“……没人愿意写这么丑的代码!”你是对的。也没人编写由CoffeeScript编译器输出的(某些)代码。这不是重点。
重点是工具可以将ES6代码转译为能够在前ES6环境中工作的代码。你可以使用块儿作用域编写代码,并从这样的功能中获益,然后让一个编译工具来掌管生成将在部署之后实际 工作 的代码。
这实际上是所有(嗯哼,大多数)ES6特性首选的迁移路径:在从前ES6到ES6的转变过程中,使用一个代码转译器将ES6代码转换为ES5兼容的代码。
Traceur
Google维护着一个称为“Traceur”[1]的项目,它的任务正是为了广泛使用ES6特性而将它转译为前ES6(大多数是ES5,但不是全部!)代码。TC39协会依赖这个工具(和其他的工具)来测试他们所规定的特性的语义。
Traceur将从我们的代码段中产生出什么?你猜对了!
{
try {
throw undefined;
} catch (a) {
a = 2;
console.log( a );
}
}
console.log( a );
所以,使用这种工具,我们可以开始利用块儿作用域,无论我们是否面向ES6,因为try/catch
从ES3那时就开始存在了(并且这样工作)。
隐含的与明确的块儿
在第三章中,在我们介绍块儿作用域时,我们认识了一些关于代码可维护性/可重构性的潜在陷阱。有什么其他的方法可以利用块儿作用域同时减少这些负面影响吗?
考虑一下let
的这种形式,它被称为“let块儿”或“let语句”(和以前的“let声明”对比来说)。
let (a = 2) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
与隐含地劫持一个既存的块儿不同,let语句为它的作用域绑定明确地创建了一个块儿。这个明确的块儿不仅更显眼,而且在代码重构方面健壮得多,从文法上讲,它通过强制所有的声明都位于块儿的顶部而产生了某种程度上更干净的代码。这使任何块儿都更易于观察,更易于知道什么属于这个作用域和什么不属于这个作用域。
作为一种模式,它是与许多人在函数作用域中采纳的方式相对照的 —— 它们手动地将所有var
声明移动/提升到函数的顶部。let语句有意地将它们放在块儿的顶部,而且如果你没有通篇到处使用let
声明,那么你的块儿作用域声明就会在某种程度上更易于识别和维护。
但是,这里有一个问题。let语句的形式没有包含在ES6中。就连官方的Traceur编译器也不接受这种形式的代码。
我们有两个选择。我们可以使用ES6合法的语法格式化,再加上一点儿代码规则:
/*let*/ { let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
但是,工具就意味着要解决我们的问题。所以另一个选项是编写明确的let语句块儿,并让工具将他转换为合理的,可以工作的代码。
所以,我建造了一个称为“let-er”[2]的工具来解决这个问题。let-er 是一个编译期代码转译器,它唯一的任务就是找到let语句形式并转译它们。它将你的代码其他部分原封不动地留下,包括任何let声明。你可以安全地将 let-er 用于ES6转译器的第一步,然后如果有需要,你可以将你的代码通过Traceur这样的东西。
另外,let-er 有一个配置标志--es6
,当它打开时(默认是关闭),会改变生成的代码的种类。与使用try/catch
的ES3填补黑科技不同的是,let-er 将拿着我们的代码并产生完全兼容ES6的代码,没有黑科技:
{
let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
所以,你可以立即开始使用 let-er,而且可以面向所有前ES6环境,当你仅关心ES6时,你可以加入配置标志并立即仅面向ES6。
而且最重要的是,你可以使用更好的和更明确的let语句形式,即便它(还)不是任何ES官方版本的一部分。
性能
让我在try/catch
的性能问题上加入最后一个快速的说明,并/或解决这个问题:“为什么不使用一个IIFE来创建作用域?”
首先,try/catch
的性能 是 慢一些,但是没有任何合理的假设表明它 必须 是这样,或者它 总是 这样。因为TC39认可的官方ES6转译器使用try/catch
,Traceur团队已经让Chrome去改进try/catch
的性能了,而且它们有很明显的动力这样做。
第二,IIFE和try/catch
不是一个公平的“苹果对苹果”的比较,因为一个包装着任意代码的函数改变了这段代码的含义,以及它的this
,return
,break
,和continue
的含义。IIFE不是一个合适一般替代品。它只能在特定的情况下手动使用。
真正的问题变成了:你是否想要使用块儿作用域。如果是,这些工具给你提供了这些选择。如果不,那就在你的代码中继续使用var
!