一般而言,所有写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语句?
总而言之,主要是这几方面的考量:
- 性能
- 不可预测
- 优化
性能问题
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提供评论支持,如果评论长时间未加载,请飞跃长城。