`
hax
  • 浏览: 953237 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

如何将let结构转换到ES3代码

    博客分类:
  • JS
阅读更多
【2011年7月12日更新】
本文所述的“with”转换方式存在一些缺点,请进一步阅读如何将let结构(block scope)转换到当前的JavaScript代码


以下是2008年11月的原文。



JavaScript 1.7开始加入了let结构。你可以在FF2以上开启let的支持:
<script type="application/javascript;version=1.7">...</script>

我们知道JS虽然是近似lexical scope,但是并非真正lex scope。其中一点就是ES3的spec所确定的scope chain机制,只作用到function这一层(虽然内部还有with和catch的特例),因此JS只有function scope而没有block scope。对于习惯block scope的程序员来说这带来一些微妙的bug隐患。比如:
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在最后一次循环时所赋值(在上面这个例子中就是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 + "\n");  
}  
  
print((x + y) + "\n");  


B. let表达式
var x = 5;  
var y = 0;  
document.write( let(x = x + 10, y = 12) x+y  + "<br>\n");  
document.write(x+y + "<br>\n");  


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]()的调用结果就和我们期望的一样,为0,100,200直到900。


下面我来讨论一下,如何在现有的ES3的引擎中模拟let的行为。如果是手写代码,自然可以规避let的问题。不过这里讨论的是有没有可能找到一个模式可以套用,这样就可以将含有let结构的代码自动转换为可在现有ES3引擎中正确执行的代码。

A. let语句转换为一层function调用:
var x = 5;  
var y = 0;  

void function(x,y){  
  print(x+y + "\n");  
}(x+10,12)

print((x + y) + "\n");  

B. let表达式转换为一层function调用:
var x = 5;  
var y = 0;  
document.write( function(x,y){return x+y}(x+10,12) + "<br>\n");  
document.write(x+y + "<br>\n");  

C. let定义转换为一层function调用:
var f = []
for (var i=0;i<10;i++) {
  void function(x){
    x = i * 100
    f.push(function () {return x})
  }()
}

这样看似就OK了。然而其实还有问题。比如,如果包含了this关键字呢?

function Test(x, y) { this.x = x; this.y = y }
Test.prototype.run = function () {
	let (y = 12) {  
	  print(this.x+y + "\n");  
	}  
	
	print((this.x + y) + "\n");  
}
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 + "\n");  
	}(this.x+10)
	
	print((this.x + this.y) + "\n");  
}

是不行的。

需要改为这样:
function Test(x, y) { this.x = x; this.y = y }
Test.prototype.run = function () {
	(function(x) {  
	print(x+this.y + "\n");  
	}).call(this, this.x+10)
	
	print((this.x + this.y) + "\n");  
}


但是这也不解决所有问题,如果是夹杂控制跳转语句的话怎么办?

比如
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 + "\n");  
}

print((x + y) + "\n");  

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,其中的continue、break、return、throw语句一律不会受到影响。


不过事情没有完美的。with的问题是,按照ES3规范来说,with语句中(if语句、for语句等)是不能有function声明的(但是允许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不会有影响,所以输出结果为:
B1
B1
B1
B2

Safari(JavaScriptCore)、Chrome(V8)、Opera(futhark)与IE(JScript)的行为是一致的。

另一方面Firefox(SpiderMonkey)和Rhino则会将with内(以及所有语句结构如if、for内的)的函数声明视作函数表达式处理。所以输出结果是:
ReferenceError: test is not defined
ReferenceError: test is not defined
C2
C2

因为本来就有分歧,所以我们不必要求let转换为with后的行为必须所有引擎一致。我们评判let转换为with后的行为是否合理,可以根据不同引擎的情况来分析。

首先在Mozilla一系的JS引擎中,将let语句转换为with语句,其内部函数声明的行为是相同的(即都视同函数表达式处理)。其次,虽然IE等引擎中并没有let语句,但我们可以认为let语句与for、if、with等语句是相似的结构,所以就let语句而言,可以预期IE等引擎下的let语句中包含函数声明的行为应该同转换为with语句后的行为一致。

至于let声明方面,假如这个let声明处于某个语句内(如if、for等结构),则情况与let语句是一样的。如果let声明不处于某个语句中,也就是直接在函数中,它就可以被安全的替换为var声明,而无需转换为with了。


最后总结一下,let结构如何转换为ES3中可以执行的代码:

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, ...
	...
}



以上。

9
11
分享到:
评论
8 楼 hax 2008-12-09  
搞不懂本文为什么有那么多人踩。。。

巧合的是,Yahoo的尼古拉斯·西·扎卡斯(名咋那像恐怖分子涅)同学这两天也提到了block level的问题:[url]
http://www.nczonline.net/blog/2008/12/04/javascript-block-level-variables/[/url]
7 楼 vb2005xu 2008-11-26  
我的IAMSESEJS什么时候可以出头啊!!!!
6 楼 hax 2008-11-26  
fregen 写道

没有转换的需求哪

普通来说是没有。
考虑这些个问题实际上是为写一个ES3.1到ES3的转换器/翻译器/预处理器做热身。
5 楼 fregen 2008-11-26  
没有转换的需求哪
4 楼 hax 2008-11-25  
dennis_zane 写道

主要是在shcheme中,let本质上是个语法糖,而scheme是lexical scope。认真看了文章,我有个疑问,js引入let的动机是什么?

动机么,其实就是完善lexical scope,加入本来缺乏的block scope啊。大多数语言都支持block scope的,JS没有理由不支持。其实按说早该如此,只能怪BE当初偷懒了。。。
3 楼 dennis_zane 2008-11-25  
hax 写道

dennis_zane 写道转为函数表达式是正常的做法,因为let本质上就是语法糖。哎,咋看了我的文章之后还是这样认为呢?let并不仅仅是语法糖,而是要求对JS的语义进行增强。本文已经证明了不是所有的let结构都能被转换为function。不过在一定程度上,可以将let结构转换为with结构。不过,这只是将let语句合理转换到可以在ES3下运行的权宜之计,一个切实的JS引擎断断不可将let只实现为with和function的语法糖,而是应该真正实现block级别的scope。

我犯了主观臆断的错误,在js大家面前献丑了 。主要是在shcheme中,let本质上是个语法糖,而scheme是lexical scope。认真看了文章,我有个疑问,js引入let的动机是什么?
2 楼 hax 2008-11-25  
dennis_zane 写道

转为函数表达式是正常的做法,因为let本质上就是语法糖。


哎,咋看了我的文章之后还是这样认为呢?
let并不仅仅是语法糖,而是要求对JS的语义进行增强。
本文已经证明了不是所有的let结构都能被转换为function。
不过在一定程度上,可以将let结构转换为with结构。
不过,这只是将let语句合理转换到可以在ES3下运行的权宜之计,一个切实的JS引擎断断不可将let只实现为with和function的语法糖,而是应该真正实现block级别的scope。
1 楼 dennis_zane 2008-11-25  
转为函数表达式是正常的做法,因为let本质上就是语法糖。

相关推荐

Global site tag (gtag.js) - Google Analytics