艾伦·斯托姆 (Alan Storm)在回应我对该with
声明的回答时的评论让我开始思考。我很少找到使用这个特定语言功能的理由,也从未想过它会如何引起麻烦。现在,我很好奇如何有效地使用with
,同时避免它的陷阱。
您从哪里发现该with
声明有用?
艾伦·斯托姆 (Alan Storm)在回应我对该with
声明的回答时的评论让我开始思考。我很少找到使用这个特定语言功能的理由,也从未想过它会如何引起麻烦。现在,我很好奇如何有效地使用with
,同时避免它的陷阱。
您从哪里发现该with
声明有用?
我今天想到了另一个用途,所以我兴奋地在网上搜索,发现了一个现有的提及:在 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
- 。
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
块语法非常相似,但在其他地方并未广泛采用。
我一直在使用 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
无论在任何特定情况下它多么诱人,您都无法安全使用。由于这里没有直接涉及这个问题,我将重复它。考虑以下代码
user = {};
someFunctionThatDoesStuffToUser(user);
someOtherFunction(user);
with(user){
name = 'Bob';
age = 20;
}
如果不仔细调查这些函数调用,就无法知道在此代码运行后程序的状态。如果user.name
已经设置,现在将是Bob
。如果没有设置,全局name
将被初始化或更改为Bob
并且user
对象将保持没有name
属性。
错误发生。如果你使用with你最终会这样做并增加你的程序失败的机会。更糟糕的是,您可能会遇到在 with 块中设置全局变量的工作代码,这可能是有意的,也可能是作者不知道构造的这种怪癖。这很像在 switch 上遇到失败,你不知道作者是否打算这样做,也没有办法知道“修复”代码是否会引入回归。
现代编程语言充满了功能。某些功能在使用多年后被发现很糟糕,应避免使用。Javascriptwith
就是其中之一。
实际上,我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);" +
"}" +
"}}}}";
是的,是的,是的。有一个非常合法的用途。手表:
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";
等等...