艾伦风暴回应我关于with
声明的回答的评论让我思考.我很少找到使用这种特定语言功能的理由,并且从未考虑过如何引起麻烦.现在,我很好奇如何有效地利用with
它,同时避免陷阱.
你在哪里发现该with
声明有用?
今天我发现了另一种用法,所以我兴奋地搜索了网络,发现现在提到它:在Block Scope中定义变量.
尽管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
块语法非常相似,但在其他地方并未广泛采用.
我一直使用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块中设置全局,故意或通过作者不知道构造的这个怪癖.这很像在开关上遇到摔倒,你不知道作者是否打算这样做,并且无法知道"修复"代码是否会引入回归.
现代编程语言充满了各种功能.经过多年使用后,一些功能被发现是坏的,应该避免.Javascript with
就是其中之一.
我实际上发现这个with
陈述最近非常有用.在我开始使用JavaScript编写的命令行控制台之前,我才真正想到这种技术.我试图模拟Firebug/WebKit控制台API,其中可以将特殊命令输入控制台,但它们不会覆盖全局范围中的任何变量.在试图克服我在Shog9的优秀答案的评论中提到的问题时,我想到了这一点.
为了达到这个效果,我使用了两个带语句来"分层"全局范围后面的范围:
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钩子都是很好用的.它不像"CloneNode"将是未定义的并且回到全球范围,除非你不顾一切并决定使其成为可能.
Crockford的速度抱怨是由一个新的背景创建.上下文通常很昂贵.我同意.但是如果你刚刚创建了一个div并且没有一些框架来设置你的css并且需要手动设置15个左右的CSS属性,那么创建一个上下文可能比变量创建和15个dereferences便宜:
var element = document.createElement("div"), elementStyle = element.style; elementStyle.fontWeight = "bold"; elementStyle.fontSize = "1.5em"; elementStyle.color = "#55d"; elementStyle.marginLeft = "2px";
等等...
您可以定义一个小辅助函数,以提供with
没有歧义的好处:
var with_ = function (obj, func) { func (obj); }; with_ (object_name_here, function (_) { _.a = "foo"; _.b = "bar"; });
由于您可以执行以下操作,因此几乎不值得:
var o = incrediblyLongObjectNameThatNoOneWouldUse; o.name = "Bob"; o.age = "50";
我没有使用,没有看到理由,也不推荐它.
问题with
在于它阻止了ECMAScript实现可以执行的许多词法优化.鉴于快速基于JIT的引擎的兴起,这个问题在不久的将来可能会变得更加重要.
它可能看起来像是with
允许更清晰的构造(比如,引入新的作用域而不是常见的匿名函数包装或替换详细的别名),但它 确实不值得.除了性能下降之外,总是存在分配错误对象的属性的危险(当在注入范围内的对象上找不到属性时)并且可能错误地引入全局变量.IIRC,后一个问题是促使Crockford建议避免的问题with
.
Visual Basic.NET有类似的With
声明.我使用它的一种常见方法是快速设置许多属性.代替:
someObject.Foo = '' someObject.Bar = '' someObject.Baz = ''
,我可以写:
With someObject .Foo = '' .Bar = '' .Baz = '' End With
这不仅仅是懒惰问题.它还使代码更易读.与JavaScript不同,它不会产生歧义,因为您必须使用.
(点)为受语句影响的所有内容添加前缀.所以,以下两个明显不同:
With someObject .Foo = '' End With
与
With someObject Foo = '' End With
前者是someObject.Foo
; 后者属于Foo
范围之外 someObject
.
我发现JavaScript缺乏区别使得它远没有Visual Basic的变体那么有用,因为模糊性的风险太高了.除此之外,with
仍然是一个强大的想法,可以提高可读性.
您可以使用with
将对象的内容作为局部变量引入块,就像使用这个小模板引擎一样.
使用"with"可以使您的代码更干燥.
请考虑以下代码:
var photo = document.getElementById('photo'); photo.style.position = 'absolute'; photo.style.left = '10px'; photo.style.top = '10px';
你可以把它干到下面:
with(document.getElementById('photo').style) { position = 'absolute'; left = '10px'; top = '10px'; }
我想这取决于你是否偏好易读性或表现力.
第一个示例更清晰,可能适用于大多数代码.但无论如何,大多数代码都非常温顺.第二个是更加模糊,但使用语言的表达性来减少代码大小和多余的变量.
我认为喜欢Java或C#的人会选择第一种方式(object.member)而喜欢Ruby或Python的人会选择后者.
我认为明显的用途是作为捷径.如果你正在初始化一个对象,你只需要输入很多"ObjectName".有点像lisp的"with-slots",可以让你写
(with-slots (foo bar) objectname "some code that accesses foo and bar"
这和写作一样
"some code that accesses (slot-value objectname 'foo) and (slot-value objectname 'bar)""
更明显的是,当你的语言允许"Objectname.foo"时,这是一个快捷方式,但仍然如此.
有了Delphi的经验,我会说使用with应该是最后的大小优化,可能通过某种javascript最小化算法执行,可以访问静态代码分析来验证其安全性.
你可以通过自由使用with语句来解决的范围问题可能是a**中的王室痛苦,我不希望任何人经历调试会话以弄清楚他们在你的代码中发生了什么,只是发现它捕获了一个对象成员或错误的局部变量,而不是你想要的全局或外部范围变量.
VB with语句更好,因为它需要点来消除作用域的歧义,但Delphi with语句是一个带有hairtrigger的加载枪,它看起来好像javascript类似于足以保证相同的警告.
建议不要使用with,并且在ECMAScript 5严格模式下禁止使用.建议的替代方法是将要访问其属性的对象分配给临时变量.
资料来源:Mozilla.org