ECMAScriptの変数のスコープが不思議な挙動をするのはよく知られた話だ。ECMAScript 5での変数のスコープは以下のいずれかに大別される(はず)。ブロックスコープがない。
- グローバルスコープ
- 関数スコープ
- catchスコープ
普通に使う変数が関数スコープしかないのは不便、ということで、MozillaはJavaScript 1.7(2006年頃)でlet文を導入した。が、しかし、他のブラウザには普及せず、Mozillaの独自拡張に留まることになった。
時は過ぎ、ECMAScript 5が制定され、次はECMAScript 6という流れになってきた。そして、ECMAScript 6にはlet文が導入されることになった(MozillaがJavaScript 1.7で導入したものとは微妙に仕様が違う)。めでたしめでたし。
ECMAScriptに静的型を付けたTypeScriptは、当初はlet文をサポートしていなかった。それが、TypeScript 1.4で、ECMAScript 6へコンパイルする場合のみlet文をサポートするようになった。
が、ECMAScript 6(のlet文)に対応していないブラウザはまだまだたくさんある(今調べてみたら、IE11ではES6のlet文に対応しているけど、Safariでは対応していないようだ)。できれば、TypeScriptのようなaltJS言語でlet文を使いつつ、コンパイル後のJavaScriptでは従来のvarを使ってほしい。
が、let文を従来のJavaScript (ECMAScript 5)に変換するのはそう自明なことではない。特に問題になるのは、ループ内の変数を、ループの外に持ち出される関数で使っている場合だ。[code lang=”js”]
var a = [];
for (var i = 0; i < 5; ++i) {
let x = i * i;
a.push(function() { return x; });
}
[/code]上のコードを下のように単純にvarを使うと挙動が変わってしまう。[code lang=”js”]
var a = [];
for (var i = 0; i < 5; ++i) {
var x = i * i;
a.push(function() { return x; });
}
[/code]このコードを正しくECMAScript 5に翻訳するには、即時実行関数(IIFE)を使って以下のようにしなければならない。[code lang=”js”]
var a = [];
for (var i = 0; i < 5; ++i) {
(function() {
var x = i * i;
a.push(function() { return x; });
}());
}
[/code]こういう非自明、というかパフォーマンスに影響するところがあるので、TypeScript 1.4ではES5へのコンパイル時にlet文がサポートされなかったのだろう。
が、しかし、TypeScript 1.5ではES5へコンパイルする場合でもlet文が部分的にサポートされるようになるようだ。「部分的に」というのは、ループが絡まない、変数のスコープのチェックと名前の変更で済む場合、のようだ。[code lang=”js”]
{
let x = 123;
console.log(x); // OK
}
console.log(x); // エラー
[/code]
ループが絡む、IIFEを使った変換が必要な場合は、
test2.ts(2,1): error TS4091: Loop contains block-scoped variable 'x' referenced by a function in the loop. This is only supported in ECMAScript 6 or higher.
というエラーが出た。
[code lang=”js”]
let a: Array<() => number> = []
for (var i = 0; i < 5; ++i) {
let x = i*i;
a.push(() => x);
}
a.forEach(console.log);
[/code]
GitHubでの該当するIssue/Pull Requestは以下のようだ。
let文のサポートとしては不完全とはいえ、varの関数スコープのせいで意図しない同名の変数を参照していた〜〜みたいな事故は防げると思われるので、積極的に使っていきたい。