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

IT技术 javascript language-features with-statement
2021-01-10 21:35:18

艾伦·斯托姆 (Alan Storm)在回应我对该with声明的回答时的评论让我开始思考。我很少找到使用这个特定语言功能的理由,也从未想过它会如何引起麻烦。现在,我很好奇如何有效地使用with,同时避免它的陷阱。

您从哪里发现该with声明有用?

6个回答

我今天想到了另一个用途,所以我兴奋地在网上搜索,发现了一个现有的提及:在 Block Scope 中定义变量

背景

尽管 JavaScript 表面上与 C 和 C++ 相似,但 JavaScript 并没有将变量范围限定在它们定义的块中:

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- 。

一个新的范围:letwith

随着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块语法非常相似,但在其他地方并未广泛采用。

IE 中的let语句支持现在真的可以挽救我的培根,我正在为是否要使用with良心挣扎真正的问题是,即使使用with作为let,由于原型链上对象的继承属性,仍然需要格外小心。例如,var toString = function () { return "Hello"; }; with ({"test":1}) { console.log(toString()); };with语句的范围内toString()Object的继承属性,因此不会调用显式定义的函数。尽管如此,仍然是一个很好的答案:-)
2021-03-11 21:35:18
实际上,上面链接的问题出现在大多数非 Mozilla 浏览器(Chrome、Safari、Opera、IE)上。
2021-03-18 21:35:18
从未想过与文字一起使用,似乎是合法的。
2021-03-24 21:35:18
这真的是死定了。我从未想过以这种方式使用 JavaScript 的作用域。完全扩展了我的编码的新领域。我希望我能投票 10 次!
2021-03-27 21:35:18
对于那些仍然反对的人,可以随时使用闭包: for (var i = 0; i < 3; ++i) { setTimeout ((function () { var num = i; return function () { alert (num); }; }) (), 10);}
2021-04-03 21:35:18

我一直在使用 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')
    )
  )
}

对于这个用例,我没有做任何分配,所以我没有与此相关的歧义问题。

该代码的“with”版本在我的机器上的运行速度比相同的“non-with”版本慢240倍这就是为什么人们说它没有合法用途。不是因为它不能在某些地方使代码更漂亮。参见基准:jsfiddle.net/sc46eeyn
2021-03-10 21:35:18
这就是我在 VB 中看到它使用的方式。(也是我所知道的唯一用途。)
2021-03-12 21:35:18
@McBrainy - 这正是您不应该使用运行速度较慢的代码的地方类型(请参阅我刚刚在此上面发表的评论)。如果您需要超级重复代码的快捷方式,您可以声明它们。例如,如果直接使用context.bezierCurveTo一百次,你可以说var bc2 = context.bezierCurveTo;,然后bc2(x,x,etc);每次你想调用它时就去这是相当快的,甚至不那么冗长,而with超级慢。
2021-03-21 21:35:18
这确实有助于减少使用画布路径的人的代码。
2021-03-23 21:35:18
这样做的一个缺点是,如果您在标记构建器对象之外的 with 块中引用一个变量,那么 js 引擎无论如何都会首先在标记构建器中搜索它,从而降低性能。
2021-04-05 21:35:18

正如我之前的评论所指出的,我认为with无论在任何特定情况下它多么诱人,您都无法安全使用由于这里没有直接涉及这个问题,我将重复它。考虑以下代码

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

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

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

错误发生。如果你使用with你最终会这样做并增加你的程序失败的机会。更糟糕的是,您可能会遇到在 with 块中设置全局变量的工作代码,这可能是有意的,也可能是作者不知道构造的这种怪癖。这很像在 switch 上遇到失败,你不知道作者是否打算这样做,也没有办法知道“修复”代码是否会引入回归。

现代编程语言充满了功能。某些功能在使用多年后被发现很糟糕,应避免使用。Javascriptwith就是其中之一。

只有在为对象的属性赋值时才会出现此问题。但是如果您仅使用它来读取值呢?我认为在这种情况下可以使用它。
2021-03-13 21:35:18
读取值有一个明确的优先规则:在范围外的变量之前检查对象上的属性。这与函数中的变量作用域没有任何不同。据我所知,赋值和“with”的真正问题在于属性赋值是否发生取决于当前对象上是否存在该属性,这是一个运行时属性,不能轻易推断通过查看代码。
2021-03-13 21:35:18
“这很像在 switch 上遇到失败,你不知道......” ——那么让我们也禁止 switch() 吧?;-p
2021-03-18 21:35:18
我想你可能就在那里,托比。写问题足以让我完全回避这个结构。
2021-04-02 21:35:18
同样的问题适用于读取 Toby 值。在上面的代码片段中,您不知道是否在用户对象上设置了 name,因此您不知道您正在读取全局名称还是用户名。
2021-04-04 21:35:18

实际上,我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);" +
    "}" +
"}}}}";
@AlexeyKozyatinskiy 你指的是哪种符号魔法?5年前... :)
2021-03-14 21:35:18
@AndyE 抱歉,这是题外话,但是您的“用 JavaScript 编写的命令行控制台”在任何地方都可用吗?
2021-03-17 21:35:18
@Adam:我不确定。ES5 仅在严格模式下为此抛出错误,因此如果您没有全局声明严格模式,这不是一个直接的问题。ES Harmony 可能会带来更大的问题,但它可能可以通过代理等一些较新的东西来解决。
2021-03-18 21:35:18
几周前,我们将 Chrome 中的控制台实现从 with 块改为一些符号魔法,因为块阻止了一些 ES6 功能:)
2021-03-30 21:35:18
但是担心 ecmascript5 会阻止您这样做。有 ecmascript 5 解决方案吗?
2021-04-09 21:35:18

是的,是的,是的。有一个非常合法的用途。手表:

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";

等等...

@Mark 是的,它们总是被定义,如果属性没有自定义样式,则使用空值或空字符串作为值
2021-03-19 21:35:18
您可以使用extendjQuery 或 Underscore.js: 中的简单方法在一行中实现相同的目标$.extend(element.style, {fontWeight: 'bold', fontSize: '1.5em', color: '#55d', marginLeft: '2px'})
2021-03-21 21:35:18
+1 因为我也认为with. 但是,在这种特殊情况下,您可以这样做:element.style.cssText="background: black ; color: blue ; border: 1px solid green"
2021-03-24 21:35:18
究竟是什么阻止了这些变量进入全局范围?仅仅是因为所有 CSS 样式总是在所有元素上定义,还是什么?
2021-03-26 21:35:18
@TrevorBurnham - 如果您假设 jQuery 可用,您只需使用它的.css()方法...
2021-04-06 21:35:18