`
hax
  • 浏览: 962668 次
  • 性别: 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本质上就是语法糖。

相关推荐

    ES3封装工具

    这样的工具可能是为了帮助开发者将现代JavaScript语法转换为ES3,以便在那些不支持新特性的老旧浏览器上运行。这种转换过程称为“转译”或“编译”,常见的工具有Babel等。 EasySysprep_3.1Final可能是这个封装工具...

    babel-preset-extendscript:Babel预设,用于将ES2015和现代JS约定转换为ES3

    Babel预设,用于将ES2015和现代JS约定转换为ES3。 旨在与Extendscript一起使用。 安装 使用NPM: $ npm install --save-dev babel-preset-extendscript 使用纱线: $ yarn add --dev babel-preset-extendscript ...

    封装工具ES3RC3

    2. **双击导入.reg**:这通常是一个注册表文件,用户双击后可以将其内容导入到系统的注册表中,可能是为了配置工具或优化系统设置。 3. **Hnwglm.cn说明.txt**:这是一个中文说明文件,可能包含了关于如何使用ES3RC...

    前端开源库-eslint-config-medikoo-es3

    `eslint-config-medikoo-es3`就是针对这些项目而设计的,确保它们也能享受到ESLint带来的代码质量保障。 ### opinonated配置 "Opinionated"在这里意味着这套配置具有强烈的主观倾向,它遵循一套预设的编码风格和...

    ES3系统封装工具

    1. **兼容性广泛**:由于"ES3系统封装工具"特别强调支持全部Windows系列,这意味着它可以处理从Windows XP到最新的Windows 10等不同版本的操作系统,满足了企业内部可能存在的多样化操作系统需求。 2. **傻瓜化操作...

    台达PLC_DVP-ES3系列编程手册_装置的定义与指令.pdf

    5. **扩展能力**:手册会说明DVP-ES3系列的扩展能力,包括最大扩展模块点数、机种代码以及脉冲输出自动复位功能。 6. **服务支持**:中达电通提供的全方位服务也是手册的一部分,包括70多个分支机构和技术团队的...

    let-er, Transpile非ES6允许块进入 ES6 ( 或者 ES3 ).zip

    let-er, Transpile非ES6允许块进入 ES6 ( 或者 ES3 ) ES6正在把 let x ="foo" JavaScript的语法,它基本上劫持任何 block 并将你的声明扩展到 block ( 而不是像ES5和 below 那样提升到包含函数) 。然而,有问题:ES6...

    es3

    Kotlin与JavaScript有很好的互操作性,可以通过Kotlin的JS编译器将Kotlin代码编译成JavaScript,因此可能与ES3有一定的关联。 在压缩包文件名称"es3-master"中,"master"通常代表这是一个项目的主分支,可能是某个...

    ES3A THRU ES3J SMAF YONGYUTAI.pdf

    ES3A THRU ES3J SMAF YONGYUTAI

    技嘉 P43 ES3G最新bios

    技嘉主板P43-ES3G最新官方2010/02/09版bios。增强内存处理速度

    GA-P43-ES3G rev1.0 F13

    技嘉EP43T刷码用bios.rom (GA-P43-ES3G rev1.0 F13)

    es3ify-webpack-plugin:一个简单的webpack插件,用于对旧版本的ie(例如ie8)进行es3ify代码。

    有所帮助,但无法转换node_modules中的模块。 所以我写了一个简单的插件来解决它。 希望它能对您有所帮助。 用法 npm install es3ify-webpack-plugin --save-dev 然后在webpack.config.js中 var es3ifyPlugin = ...

    台达PLC_DVP-ES3系列硬件及使用手册_硬件配置+连线设定+主机运作+软件设定等.pdf

    台达PLC_DVP-ES3系列是一款由台达电子科技公司推出的可编程逻辑控制器,其硬件配置丰富,适用于各种工业自动化应用场景。本手册详细介绍了该系列PLC的硬件配置、连线设定、主机运作以及软件设定等内容,旨在帮助用户...

    es6-rest-params:将 es6 其余参数编译为 es3

    将 ES6 其余参数转换为 ES3: var join = function ( joinStr , ... items ) { return items . join ( joinStr ) ;} ; 变成: var join = function ( joinStr ) { var items = [ ] . slice . call ( arguments , 1 )...

    自由天空封装工具ES3B3.0

    Easy Sysprep v3 beta 3.0是一款采用向导模式的自动化封装工具,有效地将封装步骤明确化、将各类设定规则化。保证新手能更快上手并明白封装要做什么,让老手更方便各类操作,不至于遗漏操作,可以说是真正的傻瓜式...

    前端开源库-eslint-config-medikoo-es3.zip

    这通常意味着开发者可以将这套配置导入到他们的项目中,从而确保代码风格的一致性,提高代码质量和可读性。Eslint-config-medikoo-es3可能包含了一些针对ES3特性的特定规则,以便在旧代码库中更有效地工作。 **标签...

    glsl es3 官方文档

    ### GLSL ES3官方文档知识点解析 #### 一、概述 **GLSL ES3**(OpenGL ES Shading Language Version 3.00)是专为...对于开发者而言,深入理解GLSL ES3的特性和使用方法,将有助于他们创建出更高质量的移动应用程序。

    DSDT(P43-ES3G F14).aml

    DSDT(P43-ES3G F14).aml

    SaveFile.es3

    SaveFile.es3

Global site tag (gtag.js) - Google Analytics