どのようにlet構造をES 3コードに変換しますか?


【2011年7月12日更新】ここで述べた「with」の変換方法にはいくつかの欠点があります.どのようにlet構造を現在のJavaScriptコードに変換しますか?をさらに読んでください.
以下は2008年11月の原文です.
JavaScript 1.7はlet構造への加入を開始した.あなたはFF 2以上でletのサポートを開くことができます.
<script type=「appication/javascript;version=1.7」…
私たちはJSがlexical scopeに近いが、本当にlex scopeではないことを知っています.その一つはES 3のspecによって決定されたscope chainメカニズムが、functionという層(内部にはwithとcatchの特例がありますが)にしか作用しないため、JSはblock scopeがないfunction scopeだけです.block scopeに慣れているプログラマーにとっては、これは微妙なバグの隠れた危険をもたらします.たとえば:
var f = []
for (var i=0;i<10;i++) {
  var x = i * 100
  f.push(function () {return x})
}
forサイクルにおけるvar x定義は、毎回の循環のために異なるx参照を生成しません.すべてのxは実は一つで、forサイクルに属する関数のscopeの下にある唯一のxです.したがって、各ループにおいて、イベントプロセッサの一連の要素の設定などの異なるクローズドが生成され、クローズドにxが引用されると、このxは、プログラマが期待するようにループ当時のxに対応するものではなく、すべてのクローズドのxは最後に同じxに向けられ、クローズドが実行されると、そのxは最後に同じxになる.その値は必ずxが最後のサイクルに割り当てられた値である(上記の例ではf[0]からf[9]までの呼び出し結果は900である).このような行為は多くのプログラマに理解し難いと感じさせます.
letの導入はこの問題を解決するために、さらには前向き互換性を犠牲にしています.letはキーワードであり、古いプログラムはletを変数名として使用することができます.前向き互換性を維持するためには、script要素の中でversionを明確に宣言してletを有効にしなければなりません.
letの例は以下の通りです.
A.let文:
var x = 5;  
var y = 0;  
  
let (x = x+10, y = 12) {  
  print(x+y + "
"); } print((x + y) + "
");
B.let表現
var x = 5;  
var y = 0;  
document.write( let(x = x + 10, y = 12) x+y  + "<br>
"); document.write(x+y + "<br>
");
C.let定義
var f = []
for (var i=0;i<10;i++) {
  let x = i * 100
  f.push(function () {return x})
}
AとBの出力結果は全部27、5です.
Cは前に挙げた例とまったく同じで、forの内部はvar xからlet xに変えます.このときf[0]()からf[9]()までの呼び出し結果は、900までの0,100,200と期待されています.
次に、既存のES 3のエンジンでletの動作をシミュレートする方法を検討します.手書きコードであれば、letの問題は避けられます.しかし、ここで議論したのは、モードを見つけられても使えるかどうかであり、このようにして、let構造を含むコードを既存のES 3エンジンで正しく実行できるコードに自動変換することができる.
A.let文を1つの階層のfunctionに変換して呼び出します.
var x = 5;  
var y = 0;  

void function(x,y){  
  print(x+y + "
"); }(x+10,12) print((x + y) + "
");
B.let表現を1つの階層のfunctionに変換して呼び出します.
var x = 5;  
var y = 0;  
document.write( function(x,y){return x+y}(x+10,12) + "<br>
"); document.write(x+y + "<br>
");
C.let定義は、レイヤfunction呼び出しに変換される:
var f = []
for (var i=0;i<10;i++) {
  void function(x){
    x = i * 100
    f.push(function () {return x})
  }()
}
これはよさそうです.しかし、実は問題があります.例えば、thisのキーワードが含まれているなら?
function Test(x, y) { this.x = x; this.y = y }
Test.prototype.run = function () {
	let (y = 12) {  
	  print(this.x+y + "
"); } print((this.x + y) + "
"); } new Test(5, 0).run()
同じようにヒョウタンを描くと、次のようになります.
function Test(x, y) { this.x = x; this.y = y }
Test.prototype.run = function () {
	void function(x) {  
	  print(x+this.y + "
"); }(this.x+10) print((this.x + this.y) + "
"); }
だめです
このように変更する必要があります.
function Test(x, y) { this.x = x; this.y = y }
Test.prototype.run = function () {
	(function(x) {  
	print(x+this.y + "
"); }).call(this, this.x+10) print((this.x + this.y) + "
"); }
しかし、これはすべての問題を解決しません.コントロールのジャンプ文を混ぜたらどうなりますか?
たとえば
function test(n) {
	for (var i = 0; i < n; i++) {
		let (x = i * i, y = i + 20) {
			if (x < y) continue
			else if (x == y) print(x)
			else break
		}
	}
}
scopeでblock以外にジャンプする語句があれば、直接関数を使うのはだめです.
一つの解決方法は、関数に特定の値を返してから処理することです.例えば:
function test(n) {
	for (var i = 0; i < n; i++) {
		var CONTINUE = {}, BREAK = {}
		var _result = function(x, y) {
			if (x < y) return CONTINUE
			else if (x == y) print(x)
			else return BREAK
		}(i * i, i + 20)
		if (_result === CONTINUE) continue;
		else if (_result === BREAK) break;
	}
}
しかし、この方法はより複雑で、continue、break、return、throw文を処理する必要があります.特に多層ネスティングとlabelを伴うジャンプは、どの語句が本block以外にジャンプしているかを識別し、これらの語句だけを変換する必要があります.このためにはプログラムを完全に解析しなければなりません.
もっと軽い方法がありますか?
これは元々mission impossibleですが、JSのlexical scopeは裏口を残しています.それはwithです.withは既存のscope chainの頭にbindingを追加することができる.withがあったら、AとCの等価形式(Bはlet表現ですか?それともfunctionに変換したほうが便利ですか?)にはめったに行きません.
A.let文はwithに変換されます.
var x = 5;  
var y = 0;  

with ({x:x+10, y:12}) {
  print(x+y + "
"); } print((x + y) + "
");
C.let定義からwithに変換する:
var f = []
for (var i=0;i<10;i++) {
  with ({x:undefined}) {
    x = i * 100
    f.push(function () {return x})
  }
}
with構造に変更したところ、thisの引用がないことが分かります.もっと重要なのはwithはstatementだけで、その中のcontine、break、return、throw文はすべて影響を受けません.
しかし、物事は完璧ではない.withの問題は、ES 3規格によると、with文(if文、for文など)には、function文がないということです.しかし、このようにすれば、既存のJSエンジンの多くはエラーを報告しませんが、行動に違いがあります.
var x = 'A'
void function () {
	var x = 'B1'
	try {
		test()
	} catch(e) {
		say(e)
	}
	with ({x:'C1'}) {
		try {
			test()
		} catch(e) {
			say(e)
		}
		x = 'C2'
		function test() {
			say(x)
		}
		test()
	}
	x = 'B2'
	test()
}()

function say(s) {
	alert(s)
}
IE(JScript)では、withは関数宣言のscopeに影響を及ぼさないので、出力結果は以下の通りである.
B 1
B 1
B 1
B 2
Safari(JavaScripptCore)、Chrome(V 8)、Opera(futhark)とIE(JScript)の行為は一致しています.
一方、FirefoxとRhinoはwith内(及びすべての語句構造がif、for内のような)の関数的声明を関数式処理として扱う.出力結果は:
ReferenceError:test is not defined
ReferenceError:test is not defined
C 2
C 2
もともと分岐がありますので、letがwithに変換された後の行動はすべてのエンジンが一致する必要があります.letがwithに変換された行為が合理的かどうかを判断し、異なるエンジンの状況によって分析できます.
まずMozilla系のJSエンジンでlet文をwith文に変換します.内部関数宣言の動作は同じです.次に、IEなどのエンジンにはlet文がありませんが、let文はfor、if、withなどの語句と似た構造であると考えられますので、let文については、IEなどのエンジン下のlet文に関数宣言が含まれている行為はwith文に変換されたものと一致すると予想できます.
let声明については、このlet声明がある文(if、forなどの構造)にあると、let文と同じである.let声明がある文の中にない場合、すなわち直接関数の中にいれば、withに変換することなく安全にvar宣言に置き換えられます.
最後にまとめてみます.let構造はES 3で実行できるコードにどう変換されますか?
1.let文をwith文に変換する
let (n1=exp1,n2=exp2,...) {
	...
}
変換
with ({n1:exp1,n2:exp2,...}) {
	...
}
2.let式を関数式に変換
let (n1=exp1,n2=exp2,...) exp
変換
(function(n1,n2,...){return exp}).call(this,exp1,exp2,...)
3.文blockのlet宣言をwith文に変換する
statement {
	...
	let n1 = exp1
	...
	let n2 = exp2
	...
	let n3 = exp3, n4 = exp4, ...
	...
}
変換
statement {
	with ({n1:undefined,n2:undefined,n3:undefined,n4:undefined,...}) {
		...
		n1 = exp1
		...
		n2 = exp2
		...
		n3 = exp3; n4 = exp4; ... //     “,”   “;”
		...
	}
}
4.関数の直接的なlet宣言をvar宣言に変換する
function ...() {
	...
	let n1 = exp1
	...
	let n2 = exp2
	...
	let n3 = exp3, n4 = exp4, ...
	...
}
変換
function ...() {
	...
	var n1 = exp1
	...
	var n2 = exp2
	...
	var n3 = exp3, n4 = exp4, ...
	...
}
以上です.