深入JavaScript with语句

一般而言,所有写JS的人都有一个通常的概念:“不要用with语句”。这个准则毫无疑问一直是正确的,但要说为什么的话,并不是每个人可以回答的很好。是否去回答这个为什么并无多大意义,因为只须记住结果“不去用”就完全足够。然而去深入的理由还是有的,刚好最近有人这么问起我...刚好自己想总结一下...刚好这个题目作为草稿在博客后台躺了很久...

with语句

with的初衷是为了避免冗余的对象调用:

foo.bar.baz.x = 1;
foo.bar.baz.y = 2;
foo.bar.baz.z = 3;

with(foo.bar.baz){
    x = 1;
    y = 2;
    z = 3;
}

但其实用变量替换的写法也挺简单:

var p = foo.bar.baz;
p.x = 1;
p.y = 2;
p.z = 3;

所以with似乎本来就没有存在的必要。到了如今,会去用with的人才真的是罕见。到了strict模式里,使用with会直接报错:

function foo(){'use strict'; with({});}
// Uncaught SyntaxError: Strict mode code may not include a with statement

所以with已经被彻底抛弃,人们甚至都懒的关注理由~

书中的with语句

既然是总结,我想尽可能完整一些,所以就先从书籍开头吧。有关JavaScript的书我书柜里确实不少,比Java要多出60%!好吧,这是个冷笑话。回到正题,关于基础内容的JavaScript的书,我书柜里主要有4本,下面依次拿出来说说:

《JavaScript权威指南》(第五版, David Flanagan, P109):

with (Object)
    statement

with语句用于暂时修改作用域链...这一个语句能够有效地将Object添加到作用域链的头部,然后执行statement,再把作用域链恢复到原始状态...虽然有时使用with语句比较方便,但是人们反对使用它。使用了with语句的JavaScript代码很难优化,因此它的运行速度比不使用with语句的等价代码要慢得多。而且,在with语句中的函数定义和变量初始化可能会产生令人惊讶的、和直觉相抵触的行为(这一行为以及产生的原因非常复杂,在这里我们不做解释)

以现在的眼光看《权威指南》会感觉它说的很啰嗦,一大段内容到最后还来一句相当拗口的话(当然我能理解作为译本这无法避免),到了最后还“我们不做解释”,真是尴尬...


《JavaScript高级程序设计》(第3版, Nicholas C.Zakas, P60):

with语句的作用是将代码的作用域设置到一个特定的对象中...由于大量使用with语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用with语句。

Zakas的《高级程序设计》是我JavaScript的入坑书,在with语句这个问题上也没有多深入什么,但写的还算简练,并且说法相当委婉。


《JavaScript语言精粹》(修订版, Douglas Crockford, P110),将with语句列为“糟粕”,并用例子讲述了它的不可预料性,结论上:

with语句在这个语言里存在,本身就严重影响了JavaScript处理器的速度,因为它阻断了变量名的词法作用域绑定。它的本意是好的,但如果没有它,JavaScript语言会更好一点。

老道虽也提到了速度,但更着重解释了不可预测性。


《深入理解JavaScript》(Axel Rauschmayer, P153),我手里唯一一本用了超过1页的篇幅来详细解释废弃with的原因的基础参考书。当时买这本书还是为了支持译者非常长,他曾经给我的博客Host提供过帮助,当然其实也想略微吐槽一下此书的翻译错漏:)

此书中的解释是比较全面的,所以我会以此书的解释作为基础,再加上自己的理解,总结在本文。


好了,看了那么多书,下面就进入本文的正题:

为什么不要使用with语句?

总而言之,主要是这几方面的考量:

  1. 性能
  2. 不可预测
  3. 优化

性能问题

with语句存在显而易见的性能问题,基本上所有的参考书都会提及这一点,但基本不会有什么例子说明。自己可以简单的做一个代码测试,使得对with语句的性能有更直观量化的了解。

var a = {a: {a: 1}};
function useWith(){
	with(a.a){
		for(var i = 0; i < 1000000; i++){
			a = i; 
		}
	}
}

var b = {b: {b: 1}};
function noWith(){
	for(var i = 0; i < 1000000; i++){
		b.b.b = i; 
	}
}

var t1 = new Date().getTime();
useWith()
alert(new Date().getTime() - t1);

var t2 = new Date().getTime();
noWith()
alert(new Date().getTime() - t2);

简单对一个对象属性赋值100万次,是否使with的结果差距还是很明显:

Chrome Firefox Edge IE11
Use with 603 1411 245 103
No with 2 1 3 3

当然实际使用上极少会用到百万次循环,且损耗在可接受范围内,所以其实性能损失并不是废弃with语句的真正原因。

不可预测性

使用with语句后代码产生的不可预测性是废弃with的根本原因。with强行割裂了词法作用域,将对象临时性地插入到了作用域链中。这使得出现了难以捉摸的代码。

比如最简单的例子:

function foo(a){
    with(a){
        console.log(a);
    }
}
foo("sword");       // "sword"
foo({});            // Object {}
foo({a: "sword"});  // "sword"

在这个简单的例子里,字符串"sword"和空对象没有问题,但当传入的参数是带有同名a属性的a对象时,with强行访问了a.a

这仅仅只是有一个参数的情况下,如果有很多个参数呢?当不知道调用传进来的参数带有何种属性时,多个参数间的各种属性的混乱指向可想而知,这就是“令人惊讶的、和直觉相抵触的行为”的本质。

另外,在with语句中声明的变量,并不属于with指定的参数对象:

var a ={};   
with(a){
    x = 'sword'
    var y = 'wang';
}
console.log(a.x);       // undefined
console.log(a.y);       // undefined
console.log(window.x);  // "sword"
console.log(window.y);  // "wang"

在with中声明的变量实际上是被添加到外层的function上的:

function foo(){
    with({}) { x = 'sword'; }
    console.log(x)
}
foo();  // "sword"

这点可能也与想象中有些不同。

仅仅通过标识符及其上下文,无法确定一个with中的标识符指向什么,这是with被废弃的真正原因。它强行混乱了上下文使得程序的预测和解析变得困难,从而产生了之后要说的优化问题。

代码无法优化

由于无法进行预测,代码含义一直在发生变化,不同的调用,或者即使相同的调用也会因为运行时的变化而出现偏差,从而使得代码无法被优化。

优化指两方面,一方面解析和运行变得缓慢,指的就是之前已经谈到的性能。另一方面对于代码优化和压缩工具来说,无法确定是变量还是属性,就不能进行重命名(因为属性无法被重命名)。

结语

游览了几本书,讲了一堆没啥用的内容,我果然很闲...另外,上面那个冷笑话:“JavaScript比Java要多出60%!”指的是字符数量上...好吧,天热了,自己冷自己~


评论加载中...

Disqus提供评论支持,如果评论长时间未加载,请飞跃长城。