JavaScript的“with”语句有合法用途吗?

388

Alan Storm的评论是对我关于with语句回答的回应,这让我开始思考。我很少找到使用这个特定语言功能的原因,并且从未考虑过它可能会引起麻烦。现在,我想知道如何有效地使用with,同时避免其缺点。

你在哪里发现with语句有用?


56
我从未使用过它。如果我假装它不存在,那么没有它的生活会更容易。 - Nosredna
7
或许它曾经有很多合理的用途,但现在已经无关紧要了。ES5 Strict 移除了 with,所以这个东西已经不存在了。 - Thomas Aylott
29
需要翻译的内容:Worth noting here that ES5 Strict is still optional.这里需要注意的是,ES5严格模式仍然是可选的 - Shog9
6
在 ES5 严格模式中,不是删除 'with',而是修改标准使得当 'with' 内没有找到变量时,任何赋值都会绑定到参数对象上,这样做是否更好呢? - JussiR
3
@JussiR:很可能会这样。但是这样做的问题在于,它可能会破坏旧版浏览器中的某些功能。 - Sune Rasmussen
显示剩余2条评论
33个回答

534

今天我想到了另一个用途,因此我兴奋地搜索了一下网络,并找到了现有的提及:在块作用域内定义变量

背景

尽管 JavaScript 表面上类似于 C 和 C++,但它不会将变量限定于定义它们的块内:

var name = "Joe";
if ( true )
{
   var name = "Jack";
}
// name now contains "Jack"

在循环中声明闭包是一个常见的任务,这可能会导致错误:
for (var i=0; i<3; ++i)
{
   var num = i;
   setTimeout(function() { alert(num); }, 10);
}

由于for循环不会引入新的作用域,因此所有三个函数将共享相同的“num”(值为“2”)。
一个新的作用域:let和with
引入ES6中的let语句后,可以很容易地在必要时引入新的作用域以避免这些问题。
// variables introduced in this statement 
// are scoped to each iteration of the loop
for (let i=0; i<3; ++i)
{
   setTimeout(function() { alert(i); }, 10);
}

甚至可以:

for (var i=0; i<3; ++i)
{
   // variables introduced in this statement 
   // are scoped to the block containing it.
   let num = i;
   setTimeout(function() { alert(num); }, 10);
}

直到ES6普及,这种用法仍然局限于最新的浏览器和愿意使用转译器的开发人员。但是,我们可以使用with轻松模拟此行为:

for (var i=0; i<3; ++i)
{
   // object members introduced in this statement 
   // are scoped to the block following it.
   with ({num: i})
   {
      setTimeout(function() { alert(num); }, 10);
   }
}

循环现在按预期工作,创建三个具有0到2值的单独变量。请注意,块内声明的变量不会被限定在其中,这与C ++中块的行为不同(在C中,变量必须在块的开头声明,因此在某种程度上类似)。这种行为实际上非常类似于早期版本的Mozilla浏览器中引入的let块语法,但在其他地方没有广泛采用。

18
从来没有考虑过使用"with"与文字一起使用,听起来合理。 - Matt Kantor
86
这真的非常准确。我从未想过以这种方式玩弄 JavaScript 的作用域。它为我的编码完全打开了新的领域。我希望我可以点赞十次! - kizzx2
31
对于那些仍然持反对意见的人,可以使用一个闭包来解决:for (var i = 0; i < 3; ++i) { setTimeout ((function () { var num = i; return function () { alert (num); }; }) (), 10);} - Thomas Eding
4
实际上,上面链接的问题出现在大多数非Mozilla浏览器(如Chrome、Safari、Opera和IE)中。 - Max Shawabkeh
27
如果IE也支持let语句就好了,现在我在权衡是否使用with。问题在于即使使用with作为let,仍然需要额外小心,因为对象原型链上的继承属性也会影响到执行结果。例如,var toString = function () { return "Hello"; }; with ({"test":1}) { console.log(toString()); };。在with语句的作用域中,toString()Object对象的继承属性,因此明确定义的函数不会被调用。但这仍然是个好回答 :-)。 - Andy E
显示剩余20条评论

165

我一直在使用with语句作为一种简单的局部导入方式。假设你有一种标记生成器,而不是编写:

markupbuilder.div(
  markupbuilder.p('Hi! I am a paragraph!',
    markupbuilder.span('I am a span inside a paragraph')
  )
)

你可以改为编写:

with(markupbuilder){
  div(
    p('Hi! I am a paragraph!',
      span('I am a span inside a paragraph')
    )
  )
}

针对这种使用情况,我没有进行任何赋值操作,因此我不会遇到与之相关的歧义问题。


6
这是我在VB中看到它被使用的方式。(也是我所知道的唯一用法。) - Mateen Ulhaq
2
但有一个缺点是,如果您在 with 块中引用了标记生成器对象之外的变量,js 引擎仍会首先在 markupbuilder 中搜索它,从而降低性能。 - adsy
3
这真的有助于减少使用画布路径的开发人员的代码量。 - Brian McCutchon
7
相对于相同的“非with”版本,在我的电脑上,“with”版本的代码运行速度慢了240多倍。这就是为什么人们说它没有合法用途的原因,而不是因为它不能在某些情况下让代码变得更美观。参见基准测试:http://jsfiddle.net/sc46eeyn/ - Jimbo Jonny
1
@McBrainy - 那正是你不应该使用运行速度慢得多的代码的地方(请看我刚才在这条评论上面发表的评论)。如果您需要超级重复代码的快捷方式,可以声明它们。例如,如果连续使用 context.bezierCurveTo 一百次,您可以说 var bc2 = context.bezierCurveTo; 然后每次想调用它时只需输入 bc2(x,x,etc);。这非常快速,甚至更简洁,而 with 则非常缓慢。 - Jimbo Jonny
显示剩余4条评论

87

正如我之前的评论所指出的,无论在任何情况下使用with都是不安全的,尽管它可能很诱人。由于这个问题没有被直接涵盖在这里,我会重复一下。请考虑下面的代码:

user = {};
someFunctionThatDoesStuffToUser(user);
someOtherFunction(user);

with(user){
    name = 'Bob';
    age  = 20;
}

如果不仔细调查这些函数调用,就没有办法确定程序在此代码运行后的状态。如果user.name已经设置,那么它现在将会是Bob。如果没有设置,全局的name将会被初始化或更改为Bob,而user对象将保持没有name属性。

错误总是会发生。如果你使用with,你最终会这样做,并增加程序失败的可能性。更糟糕的是,你可能会遇到在with块中设置全局变量的工作代码,无论是故意还是作者不知道这个构造的怪癖。这很像在开关语句中遇到的情况,你不知道作者是否有意这样做,也没有办法知道“修复”代码是否会引入回归。

现代编程语言功能丰富。一些特性,在多年使用后,被发现是不好的,应该避免使用。javascript的with就是其中之一。


18
仅在给对象属性赋值时才会出现此问题。但如果只是用它来读取属性值呢?我认为在这种情况下使用它是可以的。 - airportyh
10
同样的问题也适用于读取Toby的值。在上面的代码片段中,你并不知道name属性是否已经被设置在用户对象上,因此你无法确定你正在读取全局名称还是用户名称。 - Alana Storm
13
在读取值时,有一个明确的优先级规则:对象上的属性比作用域外的变量先被检查。这与函数中的变量作用域没有什么不同。就我所理解的,赋值和'with'的真正问题在于,属性是否被赋值取决于当前对象上是否存在该属性,而这是一个运行时属性,不能通过查看代码轻易推断出来。 - airportyh
2
我认为你可能是对的,Toby。写入问题足以让我完全回避这个结构。 - Alana Storm
“这很像在 switch 语句中遇到的 fall through,你完全不知道发生了什么……”--那我们也禁用 switch() 吧?;-p - Sz.

71
我最近发现with语句非常有用。直到开始我的当前项目 - 一个用JavaScript编写的命令行控制台,我从未意识到这种技术。当时我试图模拟Firebug/WebKit控制台API,其中可以在控制台中输入特殊命令,但它们不会覆盖全局范围内的任何变量。当我在评论中提到Shog9的出色答案时,我想到了这一点。
为了实现这个效果,我使用了两个with语句来“分层”全局作用域后面的作用域:
with (consoleCommands) {
    with (window) {
        eval(expression); 
    }
}

这种技术的好处是,除了性能不佳之外,它不会像with语句一样存在通常的恐惧,因为我们无论如何都在全局范围内评估 - 没有危险导致修改伪范围之外的变量。当我惊讶地发现在Chromium源代码中也使用了相同的技术时,我被激励着发帖回答。
InjectedScript._evaluateOn = function(evalFunction, object, expression) {
    InjectedScript._ensureCommandLineAPIInstalled();
    // Surround the expression in with statements to inject our command line API so that
    // the window object properties still take more precedent than our API functions.
    expression = "with (window._inspectorCommandLineAPI) { with (window) { " + expression + " } }";
    return evalFunction.call(object, expression);
}

编辑: 刚刚查看了Firebug源代码,他们将4个语句链接在一起以获得更多的层级。太疯狂了!

const evalScript = "with (__win__.__scope__.vars) { with (__win__.__scope__.api) { with (__win__.__scope__.userVars) { with (__win__) {" +
    "try {" +
        "__win__.__scope__.callback(eval(__win__.__scope__.expr));" +
    "} catch (exc) {" +
        "__win__.__scope__.callback(exc, true);" +
    "}" +
"}}}}";

2
但是担心 ECMAScript 5 会阻止您这样做。是否有 ECMAScript 5 的解决方案? - kybernetikos
@Adam:我不确定这个。在严格模式下,ES5只会抛出一个错误,所以如果你没有全局声明严格模式,这不是一个立即的问题。ES Harmony可能会带来更大的问题,但使用一些新的东西,比如代理,可能是可以解决的。 - Andy E
@AndyE 我也因为同样的原因提出了类似的问题。现在 WDG 区域已经死了 :( - Vitim.us
3
几周前,我们将Chrome中的控制台实现从with块移动到了一些符号魔法,因为with块会阻止一些ES6功能的使用。 - Alexey Kozyatinskiy
1
@AlexeyKozyatinskiy,你提到的符号魔法是什么?那是5年前的事情了... :) - ADJenks
显示剩余2条评论

58

是的,是的,还有就是这个。它有一个非常合理的用途。看:

with (document.getElementById("blah").style) {
    background = "black";
    color = "blue";
    border = "1px solid green";
}

基本上,任何其他DOM或CSS钩子都是使用with的绝佳方法。这不像“CloneNode”将未定义并返回全局范围,除非您特意决定使其成为可能。

Crockford抱怨速度的原因是with会创建一个新的上下文。上下文通常很昂贵,我同意。但是,如果您只创建了一个div,并且没有现成的框架来设置您的CSS,并且需要手动设置15个左右的CSS属性,则创建上下文可能比变量创建和15个解除引用更便宜:

var element = document.createElement("div"),
    elementStyle = element.style;

elementStyle.fontWeight = "bold";
elementStyle.fontSize = "1.5em";
elementStyle.color = "#55d";
elementStyle.marginLeft = "2px";

etc...


5
+1,我也认为with有很多合法的用途。然而,在这种情况下,你可以这样做:element.style.cssText="background: black ; color: blue ; border: 1px solid green" - GetFree
5
您可以使用jQuery或Underscore.js中的简单extend方法,在一行代码中实现同样的效果:$.extend(element.style, {fontWeight: 'bold', fontSize: '1.5em', color: '#55d', marginLeft: '2px'}) - Trevor Burnham
11
如果您假设jQuery可用,您只需使用它的.css()方法。 - nnnnnn
5
这些变量确切地被什么阻止了进入全局范围?是因为所有 CSS 样式都始终定义在所有元素上,还是其他原因? - mpen
2
@GetFree 但如果它不是全新的元素,你不能仅仅设置cssText。 - Scimonster
显示剩余2条评论

35

你可以定义一个小的辅助函数,以提供with的好处而不会有歧义:

var with_ = function (obj, func) { func (obj); };

with_ (object_name_here, function (_)
{
    _.a = "foo";
    _.b = "bar";
});

9
天啊,我的脑袋炸了!这个不会有歧义吧?得点赞,伙计! - Jarrod Dixon
@Jarrod:关于这个链接(http://is.gd/ktoZ)有什么好笑的?使用该网站的大多数人都比我聪明,所以如果我错了,请原谅,但这似乎是错误信息。 - raven
14
这只是一种更长且更难理解的做法:var _ = obj_name_here; _.a="foo"; _.b="bar"。 - Rene Saarsoo
4
Rene:这样做会将“_”变量暴露给外部作用域,可能导致潜在的错误。还会暴露用于计算对象参数的任何临时变量。 - John Millikin
30
使用with_会导致更多的错误,因为它是(function(_){ _.a="foo"; })(object_here);的混乱重复版本。(function(_){ _.a="foo"; })(object_here);是模拟C/Java风格块的标准方法,建议使用该方法代替with_ - mk.

26

似乎不值得这样做,因为你可以执行以下操作:

var o = incrediblyLongObjectNameThatNoOneWouldUse;
o.name = "Bob";
o.age = "50";

1
这对我来说毫无意义。虽然我不是JavaScript专家。 - JeroenEijkhof
@WmasterJ 为了清晰起见,请参阅此帖子:http://yuiblog.com/blog/2006/04/11/with-statement-considered-harmful/ - Dennis
8
使用with的情况不仅限于长变量名。 - Chris

19

我从不使用 with,也没有理由使用它,并且不建议使用。

with 的问题在于它防止 ECMAScript 实现执行许多词法优化。随着快速 JIT 引擎的兴起,这个问题在不久的将来可能会变得更加重要。

虽然 with 看起来可以创建更清晰的结构(例如,引入新作用域而不是常见的匿名函数包装器或替换冗长的别名),但它真的不值得。除了性能下降外,还存在将属性分配给错误对象(当注入范围中的对象上未找到属性时)并可能出现错误引入全局变量的风险。我IRC上记得,后者是 Crockford 建议避免使用 with 的原因之一。


6
性能妖怪经常被提起,几乎和全局变量一样频繁......这总让我感到奇怪,因为我们谈论的是 JavaScript。你会认为性能损失应该是非常明显的,才值得那么多关注,但是......如果你有任何硬数据可以说明 with(){} 这样的语句在现代浏览器中的成本,就像其他回答中所给出的那些,我很想看看! - Shog9
6
在JavaScript中,为什么这个情况很奇怪?:) 是的,这是戏剧性的。想一想-实现需要评估括号内的表达式,将其转换为对象,将其插入到当前作用域链的前面,在块内评估语句,然后将作用域链恢复到正常状态。这是一项非常繁重的工作,比可以转换为高度优化的低级代码的简单属性查找要多得多。这是我刚刚制作的一个非常简单的基准测试(如果您发现任何错误,请告诉我),展示了两者之间的区别-https://gist.github.com/c36ea485926806020024 - kangax
5
@kangax:我来自C++背景,在那里,许多程序员通常都会迷恋于代码中的小效率问题,即使这些问题实际上对更大的例程或程序的性能没有明显的影响。在JavaScript的环境下,这似乎有些奇怪,因为该例程的性能很大程度上取决于VM实现。我看到过一些JS程序员会避免使用匿名函数,因为他们担心它的设置成本,但这似乎是个例外而不是规则,只在代码非常敏感的区域才会使用。 - Shog9
5
说到使用with(){}语句的成本,你是完全正确的:在我测试过的每个浏览器中,使用with建立新作用域的代价都非常高昂。你应该避免在任何经常调用的代码中使用它。此外,在with()作用域内执行的任何代码都会对Chrome产生明显的影响。有趣的是,在with()块中的代码在IE中表现最佳: 在IE6和IE8虚拟机中排除设置成本后,with()提供了最快的成员访问方式(尽管这些虚拟机整体上是最慢的)。谢谢,好东西... - Shog9
5
顺便说一句:这里是将设置成本排除在外的相同测试集:http://jsbin.com/imidu/edit。在Chrome中,使用`with()`进行变量访问几乎慢了一个数量级,在IE中则快了两倍以上...! - Shog9
显示剩余2条评论

15

Visual Basic.NET有一个类似的With语句。我常用的一种方式是快速设置多个属性。不用写成这样:

someObject.Foo = ''
someObject.Bar = ''
someObject.Baz = ''

我可以编写:

With someObject
    .Foo = ''
    .Bar = ''
    .Baz = ''
End With

这不仅仅是懒惰的问题。它还可以使代码更易读。与 JavaScript 不同的是,它不会出现歧义,因为您必须在语句影响到的所有内容前加上“.”(点号)。因此,以下两个内容是明显不同的:

With someObject
    .Foo = ''
End With

vs.

可以翻译为“对比”、“与”,常用于表示两个事物之间的比较或对抗关系。在IT技术领域中,通常用于表示不同软件、编程语言等之间的比较,例如 Python vs. Java(Python与Java的比较)。
With someObject
    Foo = ''
End With

前者是someObject.Foo; 后者是someObject之外的作用域中的Foo

我发现JavaScript缺乏区别,使得它比Visual Basic的variant要不太实用,因为存在歧义的风险太高。除此之外,with仍然是一个能够提高可读性的强大思想。


2
确实没错。但这并没有回答他的问题。所以这是离题的。 - Allain Lalonde
6
这也在我的脑海中闪过,有人必须说出它。为什么 JavaScript 不能也有点呢? - Carson Myers
同意,点符号表示法更好,希望 JavaScript 使用它。+1 - user2191247

8
您可以使用with将对象的内容作为局部变量引入到块中,就像这个小型模板引擎所做的那样。

1
该技术也被应用于underscore.js的[_.template()] (http://underscorejs.org/#template)中。 - Peter V. Mørch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接