ES6 随笔:函数与块级作用域

阅读 Kyle Simpson 《You don't JS: Scope and Closures》第三章过程中的一些随笔

作用域

作者提到了 ES5 中有三种方式实现作用域:function、with 和鲜为人知的 try/catch 。with 已经被淘汰,function 方式可以看看之前翻译的经典文章《深入理解 JavaScript 模块模式 》,而最后一种 hack 真是让人眼前一亮。他在附录 B 中也提到 google 的 Traceur 也是这么实现的,我试了一下,发现 Traceur 与 Babel 现在都没有采用这种方式了,而是直接检测 shadow 冲突再使用不同的变量名,这可能是考虑到性能的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ES6 代码
// ...
let a = 1;
{
let a = 2;
{
let a = 3;
}
}
// ...
// Try/Catch 方式
// ...
try {throw 1} catch(a) {
try {throw 2} catch(a) {
try {throw 3} catch(a) {
}
}
}
// ...
// Traceur
// ...
var a = 1;
{
var a$__0 = 2;
{
var a$__1 = 3;
}
}
// ...
// Babel
// ...
var a = 1;
{
var _a = 2;
{
var _a2 = 3;
}
}
// ...

TDZ

有意思的是,这几种 hack 都不能解决 let 变量不许提升 (hoist) 的问题。

1
2
console.log(a); // ReferenceError
let a;

let 声明行到它所在 block 最开始之间的区域被称为 TDZ “Temporal dead zone”,正常情况下,可以考虑用回上面的方法 hack ,把 TDZ 当做一个 block,但是这里提到了一种坑爹的情况,函数。

1
2
3
4
5
function foo() {
x; // 合法
}
let x;
foo();

1
2
3
4
5
function foo() {
x; // 不合法
}
foo();
let x;

1
2
3
4
5
foo();
let x;
function foo() {
x; // 不合法
}

这么一来静态分析出错误就变得很麻烦,在 Traceur 的一个 issue 中 @arv 提到了牺牲运行时来检测的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES6
let a = f();
const b = 2;
function f() { return b; }
// hack
$traceurRuntime.UNITIALIZED = {};
$traceurRuntime.assertInitialized = function(v) {
if (v === UNITIALIZED) throw new ReferenceError();
return v;
};
var a = $traceurRuntime.UNITIALIZED;
var b = $traceurRuntime.UNITIALIZED;
a = f();
b = 2;
function f() {
return $traceurRuntime.assertInitialized(b);
}

这在 Babel 中需要开启 es6.blockScopingTDZ 特性:

1
$ babel-node --optional es6.blockScopingTDZ test.js

(3 月 19 日 补)
注意 for 循环闭包的坑,现在还没有好的 polyfill 方案

1
2
3
4
5
6
7
// 0 1 2 3 4
for (let i = 0; i < 5; i += 1) {
// 忽略我在循环中声明函数
setTimeout(function timer() {
console.log(i);
}, 1*1000);
}

块级作用域

作者提到几个块级作用域的好处,比较有意思的一个是对垃圾回收的优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function process(data) {
// ...
}
var someReallyBigData = {
// ...
};
process(someReallyBigData);
var btn = document.getElementById('a_button');
btn.addEventListener('click', function(evt) {
console.log('button clicked');
}, false);

这里 btn 的回调函数虽然没有直接用到 someReallyBigData ,但 JS 引擎很可能会继续保留 someReallyBigData 因为闭包使回调函数引用了 someReallyBigData 所在的作用域。ES6 中,使用显式的块则可以向引擎明确回收时机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function process(data) {
// ...
}
{
let someReallyBigData = {
// ...
};
process(someReallyBigData);
}// 这里就可以回收 someReallyBigData 了
var btn = document.getElementById('a_button');
btn.addEventListener('click', function(evt) {
console.log('button clicked');
}, false);

您还在局域网。 ——来自隔墙相望的评论