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

Alan Storm在回答关于with声明的回答时的评论使我想到了。 我很lessfind使用这种特定语言function的理由,也从来没有考虑过如何引起麻烦。 现在,我很好奇,如何能够有效地使用,而避免它的陷阱。

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

今天我又发现了另一个用途,所以我兴奋地search了网页,发现了一个现有的提示: 在块范围内定义variables 。

背景

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

 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

通过在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普遍可用之前,这种使用仍然限于愿意使用转发器的最新浏览器和开发者。 但是,我们可以使用以下方法轻松地模拟此行为:

 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); } } 

循环现在按预期工作,创build三个单独的variables,值为0到2.请注意, 声明的variables没有作用域,与C ++中块的行为不同(在C中,variables必须在一个块,所以在某种程度上它是相似的)。 这个行为实际上和Mozilla浏览器早期版本中引入的let语法非常相似,但是在其他地方没有被广泛采用。

我一直使用with语句作为范围导入的简单forms。 假设您有某种标记生成器。 而不是写作:

 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不pipe在任何情况下有多诱人。 既然这个问题没有直接覆盖,我会重复一遍。 考虑下面的代码

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

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

错误发生。 如果你你一起使用最终会这样做,并增加你的程序失败的机会。 更糟的是,你可能会遇到工作代码,设置一个全局的块或者故意或作者不知道这个构造的怪癖。 这很像在交换机上遇到的问题,你不知道作者是否打算这样做,也无法知道“修复”代码是否会引入回归。

现代编程语言充斥着各种function。 一些function,经过多年的使用,被发现是坏的,应该避免。 Javascript的是其中之一。

实际上,我发现最近的声明是非常有用的。 直到我开始当前的项目 – 用JavaScript编写的命令行控制台之前,这种技术从来没有真正发生过。 我试图模拟Firebug / WebKit控制台API,可以在控制台中input特殊命令,但是它们不覆盖全局范围中的任何variables。 当我试图克服我在Shog9的优秀回答中提到的问题时,我想到了这个问题。

为了达到这个效果,我使用了两个语句来“覆盖”全局范围之后的范围:

 with (consoleCommands) { with (window) { eval(expression); } } 

关于这种技术的好处在于,除了性能上的缺点之外,它不会遭受通常的担心,因为我们正在全球范围内进行评估 – 在我们的伪范围之外没有variables的危险被修改。

当我惊奇地发现这个答案的时候,我设法find了其他地方使用的相同技术–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”将不被定义,而是要回到全球范围,除非你不顾一切地决定使之成为可能。

克罗克福德的速度抱怨是,一个新的背景是由创build。 上下文通常是昂贵的。 我同意。 但是如果你只是创build了一个div,并没有一些框架来设置你的CSS,并且需要手动设置15个左右的CSS属性,那么创build上下文可能会更便宜,然后创buildvariables和15个解引用:

 var element = document.createElement("div"), elementStyle = element.style; elementStyle.fontWeight = "bold"; elementStyle.fontSize = "1.5em"; elementStyle.color = "#55d"; elementStyle.marginLeft = "2px"; 

等等…

你可以定义一个小的帮助函数来提供没有含糊的好处:

 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引擎的兴起,这个问题在不久的将来可能会变得更加重要。

它可能看起来像允许更清洁的构造(当引入一个新的范围而不是一个普通的匿名函数包装或replace冗余别名),但它真的不值得 。 除了性能下降之外,总是有分配给错误对象的属性的危险(当在注入范围内的对象上没有find属性时),并且可能错误地引入全局variables。 IIRC,后一个问题是激励Crockford推荐避免with

Visual Basic.NET有一个类似的声明。 我使用它的一个更常见的方法是快速设置一些属性。 代替:

 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 ; 后者 someObject的范围内是Foo

我发现JavaScript缺乏区分使其远不如Visual Basic的变体有用,因为歧义的风险太高。 除此之外,依然是一个强大的想法,可以使更好的可读性。

使用“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'; } 

我想这取决于你是否喜欢易读性或performance力。

第一个例子更清晰,可能推荐大多数代码。 但是大多数代码都是非常驯服的。 第二个是比较模糊的,但是使用语言的expression特性来减less代码的大小和多余的variables。

我想像Java或C#的人会select第一种方式(object.member),而那些喜欢Ruby或Python的人会select后者。

您可以使用将对象的内容作为局部variables引入块,就像使用这个小型模板引擎一样 。

我认为明显的用途是一个捷径。 如果你正在初始化一个对象,你只需要保存input大量的“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最小化algorithm执行,访问静态代码分析来validation其安全性。

你可以通过自由使用with语句来考虑的范围问题可能是a **中的王室痛苦,我不想让任何人去体验一个debugging会话来弄清楚他在你的代码中是怎么回事,只是发现它捕获了一个对象成员或错误的局部variables,而不是你想要的全局或外部作用域variables。

用VB语句比较好,因为它需要用点来消除歧义范围,但Delphi with statement是一个带有hangerrigger的加载枪,它在我看来好像JavaScript足够类似以保证相同的警告。

with语句可用于减less代码大小或私有类成员,例如:

 // demo class framework var Class= function(name, o) { var c=function(){}; if( o.hasOwnProperty("constructor") ) { c= o.constructor; } delete o["constructor"]; delete o["prototype"]; c.prototype= {}; for( var k in o ) c.prototype[k]= o[k]; c.scope= Class.scope; c.scope.Class= c; c.Name= name; return c; } Class.newScope= function() { Class.scope= {}; Class.scope.Scope= Class.scope; return Class.scope; } // create a new class with( Class.newScope() ) { window.Foo= Class("Foo",{ test: function() { alert( Class.Name ); } }); } (new Foo()).test(); 

如果你想修改范围,with-statement是非常有用的,你可以在运行时使用你自己的全局范围。 你可以在其上放置常量,或者经常使用某些辅助函数,如“toUpper”,“toLower”或“isNumber”,“clipNumber”aso ..

关于我经常读到的糟糕的性能:范围函数不会对性能产生任何影响,事实上,在我的FF范围函数运行得更快,然后是一个unscoped:

 var o={x: 5},r, fnRAW= function(a,b){ return a*b; }, fnScoped, s, e, i; with( o ) { fnScoped= function(a,b){ return a*b; }; } s= Date.now(); r= 0; for( i=0; i < 1000000; i++ ) { r+= fnRAW(i,i); } e= Date.now(); console.log( (es)+"ms" ); s= Date.now(); r= 0; for( i=0; i < 1000000; i++ ) { r+= fnScoped(i,i); } e= Date.now(); console.log( (es)+"ms" ); 

所以用上面提到的方式使用with-statement对性能没有负面影响,但是这样做会降低代码的大小,影响移动设备的内存使用。

不build议使用with,禁止在ECMAScript 5严格模式下使用。 build议的替代方法是将要访问其属性的对象分配给临时variables。

来源:Mozilla.org

在许多实现中使用也会使代码变得更慢,因为现在所有东西都被包装在一个额外的查找范围中。 在JavaScript中使用没有合法的理由。

我认为with的用处可能取决于你的代码编写的程度。 例如,如果您编写的代码如下所示:

 var sHeader = object.data.header.toString(); var sContent = object.data.content.toString(); var sFooter = object.data.footer.toString(); 

那么你可以争辩说with通过这样做将提高代码的可读性:

 var sHeader = null, sContent = null, sFooter = null; with(object.data) { sHeader = header.toString(); sContent = content.toString(); sFooter = content.toString(); } 

相反,可能会认为你违反了得墨忒耳法 ,但是,也可能不是。 我离题=)。

最重要的是,知道道格拉斯克罗克福德build议不要使用。 我希望你能看看他的博客文章,关于与其替代品在这里 。

我认为在将模板语言转换为JavaScript时,with-statement可以派上用场。 比如base2中的 JST ,但是我更经常看到它。

我同意一个人可以编程这个没有与声明。 但是因为它没有给出任何问题,所以这是一个合法的用途。

我认为对象字面使用是有趣的,就像使用闭包的替代方法

 for(var i = nodes.length; i--;) { // info is namespaced in a closure the click handler can access! (function(info) { nodes[i].onclick = function(){ showStuff(info) }; })(data[i]); } 

或者等同于封闭的声明

 for(var i = nodes.length; i--;) { // info is namespaced in a closure the click handler can access! with({info: data[i]}) { nodes[i].onclick = function(){ showStuff(info) }; } } 

我认为真正的风险是不小心缩小了不属于with语句的variables,这就是为什么我喜欢传入的对象字面值,你可以看到在代码中添加的上下文中究竟是什么。

我创build了一个“合并”函数,用with语句消除了一些含糊的含义:

 if (typeof Object.merge !== 'function') { Object.merge = function (o1, o2) { // Function to merge all of the properties from one object into another for(var i in o2) { o1[i] = o2[i]; } return o1; }; } 

我可以用同样的方式来使用它,但是我知道它不会影响我不打算影响的任何范围。

用法:

 var eDiv = document.createElement("div"); var eHeader = Object.merge(eDiv.cloneNode(false), {className: "header", onclick: function(){ alert("Click!"); }}); function NewObj() { Object.merge(this, {size: 4096, initDate: new Date()}); } 

For some short code pieces, I would like to use the trigonometric functions like sin , cos etc. in degree mode instead of in radiant mode. For this purpose, I use an AngularDegree object:

 AngularDegree = new function() { this.CONV = Math.PI / 180; this.sin = function(x) { return Math.sin( x * this.CONV ) }; this.cos = function(x) { return Math.cos( x * this.CONV ) }; this.tan = function(x) { return Math.tan( x * this.CONV ) }; this.asin = function(x) { return Math.asin( x ) / this.CONV }; this.acos = function(x) { return Math.acos( x ) / this.CONV }; this.atan = function(x) { return Math.atan( x ) / this.CONV }; this.atan2 = function(x,y) { return Math.atan2(x,y) / this.CONV }; }; 

Then I can use the trigonometric functions in degree mode without further language noise in a with block:

 function getAzimut(pol,pos) { ... var d = pos.lon - pol.lon; with(AngularDegree) { var z = atan2( sin(d), cos(pol.lat)*tan(pos.lat) - sin(pol.lat)*cos(d) ); return z; } } 

This means: I use an object as a collection of functions, which I enable in a limited code region for direct access. I find this useful.

I just really don't see how using the with is any more readable than just typing object.member. I don't think it's any less readable, but I don't think it's any more readable either.

Like lassevk said, I can definitely see how using with would be more error prone than just using the very explicit "object.member" syntax.

It's good for putting code that runs in a relatively complicated environment into a container: I use it to make a local binding for "window" and such to run code meant for a web browser.

You got to see the validation of a form in javascript at W3schools http://www.w3schools.com/js/js_form_validation.asp where the object form is "scanned" through to find an input with name 'email'

But i've modified it to get from ANY form all the fields validate as not empty, regardless of the name or quantity of field in a form. Well i've tested only text-fields.

But the with() made things simpler. 代码如下:

 function validate_required(field) { with (field) { if (value==null||value=="") { alert('All fields are mandtory');return false; } else { return true; } } } function validate_form(thisform) { with (thisform) { for(fiie in elements){ if (validate_required(elements[fiie])==false){ elements[fiie].focus(); elements[fiie].style.border='1px solid red'; return false; } else {elements[fiie].style.border='1px solid #7F9DB9';} } } return false; } 

CoffeeScript's Coco fork has a with keyword, but it simply sets this (also writable as @ in CoffeeScript/Coco) to the target object within the block. This removes ambiguity and achieves ES5 strict mode compliance:

 with long.object.reference @a = 'foo' bar = @b 

Here's a good use for with : adding new elements to an Object Literal, based on values stored in that Object. Here's an example that I just used today:

I had a set of possible tiles (with openings facing top, bottom, left, or right) that could be used, and I wanted a quick way of adding a list of tiles which would be always placed and locked at the start of the game. I didn't want to keep typing types.tbr for each type in the list, so I just used with .

 Tile.types = (function(t,l,b,r) { function j(a) { return a.join(' '); } // all possible types var types = { br: j( [b,r]), lbr: j([l,b,r]), lb: j([l,b] ), tbr: j([t,b,r]), tbl: j([t,b,l]), tlr: j([t,l,r]), tr: j([t,r] ), tl: j([t,l] ), locked: [] }; // store starting (base/locked) tiles in types.locked with( types ) { locked = [ br, lbr, lbr, lb, tbr, tbr, lbr, tbl, tbr, tlr, tbl, tbl, tr, tlr, tlr, tl ] } return types; })("top","left","bottom","right"); 

You can use with to avoid having to explicitly manage arity when using require.js:

 var modules = requirejs.declare([{ 'App' : 'app/app' }]); require(modules.paths(), function() { with (modules.resolve(arguments)) { App.run(); }}); 

Implementation of requirejs.declare:

 requirejs.declare = function(dependencyPairs) { var pair; var dependencyKeys = []; var dependencyValues = []; for (var i=0, n=dependencyPairs.length; i<n; i++) { pair = dependencyPairs[i]; for (var key in dependencyPairs[i]) { dependencyKeys.push(key); dependencyValues.push(pair[key]); break; } }; return { paths : function() { return dependencyValues; }, resolve : function(args) { var modules = {}; for (var i=0, n=args.length; i<n; i++) { modules[dependencyKeys[i]] = args[i]; } return modules; } } } 

As Andy E pointed out in the comments of Shog9's answer, this potentially-unexpected behavior occurs when using with with an object literal:

 for (var i = 0; i < 3; i++) { function toString() { return 'a'; } with ({num: i}) { setTimeout(function() { console.log(num); }, 10); console.log(toString()); // prints "[object Object]" } } 

Not that unexpected behavior wasn't already a hallmark of with .

If you really still want to use this technique, at least use an object with a null prototype.

 function scope(o) { var ret = Object.create(null); if (typeof o !== 'object') return ret; Object.keys(o).forEach(function (key) { ret[key] = o[key]; }); return ret; } for (var i = 0; i < 3; i++) { function toString() { return 'a'; } with (scope({num: i})) { setTimeout(function() { console.log(num); }, 10); console.log(toString()); // prints "a" } } 

But this will only work in ES5+. Also don't use with .

I am working on a project that will allow users to upload code in order to modify the behavior of parts of the application. In this scenario, I have been using a with clause to keep their code from modifying anything outside of the scope that I want them to mess around with. The (simplified) portion of code I use to do this is:

 // this code is only executed once var localScope = { build: undefined, // this is where all of the values I want to hide go; the list is rather long window: undefined, console: undefined, ... }; with(localScope) { build = function(userCode) { eval('var builtFunction = function(options) {' + userCode + '}'); return builtFunction; } } var build = localScope.build; delete localScope.build; // this is how I use the build method var userCode = 'return "Hello, World!";'; var userFunction = build(userCode); 

This code ensures (somewhat) that the user-defined code neither has access to any globally-scoped objects such as window nor to any of my local variables through a closure.

Just as a word to the wise, I still have to perform static code checks on the user-submitted code to ensure they aren't using other sneaky manners to access global scope. For instance, the following user-defined code grabs direct access to window :

 test = function() { return this.window }; return test(); 

My

 switch(e.type) { case gapi.drive.realtime.ErrorType.TOKEN_REFRESH_REQUIRED: blah case gapi.drive.realtime.ErrorType.CLIENT_ERROR: blah case gapi.drive.realtime.ErrorType.NOT_FOUND: blah } 

boils down to

 with(gapi.drive.realtime.ErrorType) {switch(e.type) { case TOKEN_REFRESH_REQUIRED: blah case CLIENT_ERROR: blah case NOT_FOUND: blah }} 

Can you trust so low-quality code? No, we see that it was made absolutely unreadable. This example undeniably proves that there is no need for with-statement, if I am taking readability right 😉