Are there legitimate uses for JavaScript's “with” statement?
阿兰·斯托姆对我关于
你在哪里发现
今天我又遇到了另一种用法,所以我兴奋地搜索了一下Web,发现了一个关于它的现有提及:在块范围内定义变量。
背景JavaScript尽管与C和C++的表面相似,但对它们定义的块没有范围变量:
1 2 3 4 5 6 | var name ="Joe"; if ( true ) { var name ="Jack"; } // name now contains"Jack" |
在循环中声明一个闭包是一项常见的任务,这可能导致错误:
1 2 3 4 5 | for (var i=0; i<3; ++i) { var num = i; setTimeout(function() { alert(num); }, 10); } |
因为for循环没有引入新的作用域,所以这三个函数将共享同一个
随着ES6中引入了
1 2 3 4 5 6 | // 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); } |
甚至:
1 2 3 4 5 6 7 | 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普及之前,这种使用仍然局限于最新的浏览器和愿意使用蒸腾器的开发人员。但是,我们可以使用
1 2 3 4 5 6 7 8 9 | 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浏览器中引入的
我一直将WITH语句用作作用域导入的简单形式。假设您有某种类型的标记生成器。而不是写作:
1 2 3 4 5 | markupbuilder.div( markupbuilder.p('Hi! I am a paragraph!', markupbuilder.span('I am a span inside a paragraph') ) ) |
你可以写:
1 2 3 4 5 6 7 | with(markupbuilder){ div( p('Hi! I am a paragraph!', span('I am a span inside a paragraph') ) ) } |
对于这个用例,我不做任何分配,所以我没有与之相关的模糊性问题。
正如我之前的评论所指出的,无论在任何给定的情况下有多诱人,我认为你都不能安全地使用
1 2 3 4 5 6 7 8 | user = {}; someFunctionThatDoesStuffToUser(user); someOtherFunction(user); with(user){ name = 'Bob'; age = 20; } |
如果不仔细研究这些函数调用,就无法知道运行此代码后程序的状态。如果
错误发生了。如果与一起使用,最终将执行此操作,并增加程序失败的几率。更糟糕的是,您可能会遇到在WITH块中设置全局的工作代码,不管是故意的还是通过作者不知道构造的这种奇怪之处。这很像是碰到了一个开关,你不知道作者是否打算这样做,也不知道"修正"代码是否会引入回归。
现代编程语言充满了特性。使用多年后,发现一些功能不好,应避免使用。javascript的
最近我发现
为了达到这一效果,我使用了两个WITH语句将作用域"分层"到全局作用域之后:
1 2 3 4 5 | with (consoleCommands) { with (window) { eval(expression); } } |
这项技术的好处在于,除了性能方面的缺点外,它不会受到
令我惊讶的是,当我发现其他地方也使用了同样的技术——铬源代码时,我被激励去发布这个答案。
1 2 3 4 5 6 7 | 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个语句链接在一起,以获得更多的层。疯子!
1 2 3 4 5 6 7 | 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);" + "}" + "}}}}"; |
是的,是的,是的。有一个非常合法的使用。手表:
1 2 3 4 5 | with (document.getElementById("blah").style) { background ="black"; color ="blue"; border ="1px solid green"; } |
基本上,任何其他的dom或css钩子都是with的奇妙用法。这不像"克隆诺德"是未定义的,它会回到全球范围,除非你不按自己的方式决定让它成为可能。
克罗克福德的速度抱怨是,一个新的环境是由创造的。环境通常是昂贵的。我同意。但是,如果您刚刚创建了一个DIV,并且没有现成的用于设置CSS的框架,并且需要手动设置大约15个CSS属性,那么创建上下文可能比创建变量和15个解引用便宜:
1 2 3 4 5 6 7 | var element = document.createElement("div"), elementStyle = element.style; elementStyle.fontWeight ="bold"; elementStyle.fontSize ="1.5em"; elementStyle.color ="#55d"; elementStyle.marginLeft ="2px"; |
等。。。
您可以定义一个小的助手函数来提供
1 2 3 4 5 6 7 | var with_ = function (obj, func) { func (obj); }; with_ (object_name_here, function (_) { _.a ="foo"; _.b ="bar"; }); |
这似乎不值得,因为您可以执行以下操作:
1 2 3 | var o = incrediblyLongObjectNameThatNoOneWouldUse; o.name ="Bob"; o.age ="50"; |
我从来没有用过,没有理由,也不推荐。
它可能看起来像
Visual Basic.NET有一个类似的
1 2 3 | someObject.Foo = '' someObject.Bar = '' someObject.Baz = '' |
我可以写:
1 2 3 4 5 | With someObject .Foo = '' .Bar = '' .Baz = '' End With |
这不仅仅是懒惰的问题。它还可以使代码更易于阅读。与JavaScript不同,它不存在歧义,因为您必须在受语句影响的所有内容前面加上一个EDOCX1(dot)。因此,以下两个明显不同:
1 2 3 | With someObject .Foo = '' End With |
VS
1 2 3 | With someObject Foo = '' End With |
前者是
我发现JavaScript缺乏区分性使得它远不如VisualBasic的变体有用,因为模棱两可的风险太高。除此之外,
您可以使用
使用"with"可以使代码更加干燥。
请考虑以下代码:
1 2 3 4 | var photo = document.getElementById('photo'); photo.style.position = 'absolute'; photo.style.left = '10px'; photo.style.top = '10px'; |
您可以将其干燥至以下状态:
1 2 3 4 5 | with(document.getElementById('photo').style) { position = 'absolute'; left = '10px'; top = '10px'; } |
我想这取决于你是喜欢易读性还是表达性。
第一个示例更清晰,可能推荐用于大多数代码。但无论如何,大多数代码都相当温顺。第二种方法有点模糊,但使用语言的表达特性来减少代码大小和多余的变量。
我想,喜欢Java或C语言的人会选择第一种方式(对象。成员),而喜欢Ruby或Python的人会选择后者。
有了Delphi的经验,我会说,使用with应该是最后的选择,可能由某种能够访问静态代码分析以验证其安全性的javascript最小化算法执行。
在a**中,自由使用with语句可能会导致范围界定问题,我不希望任何人体验调试会话来了解他是什么。在您的代码中进行,结果发现它捕获了一个对象成员或错误的局部变量,而不是您想要的全局或外部范围变量。
带语句的vb更好,因为它需要点来消除作用域的歧义,但是带语句的delphi是一个带发扳机的加载枪,在我看来,javascript中的一个非常相似,足以保证相同的警告。
我认为明显的用途是作为一种捷径。如果你正在初始化一个对象,你只需输入大量的"objectname",类似于lisp的"with s lot s",它允许你写
1 2 | (with-slots (foo bar) objectname "some code that accesses foo and bar" |
和写作一样
1 | "some code that accesses (slot-value objectname 'foo) and (slot-value objectname 'bar)"" |
更明显的是,当您的语言允许"objectname.foo"但仍然允许时,为什么这是一个快捷方式。
不建议与一起使用,并且在ECMAScript 5严格模式下禁止使用。建议的替代方法是将要访问其属性的对象分配给临时变量。
来源:mozilla.org
WITH语句可用于减小代码大小或用于私有类成员,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // 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语句非常有用,如果您想修改作用域,那么拥有自己的全局作用域是必要的,您可以在运行时对其进行操作。您可以在它上面放置常量或某些常用的助手函数,例如"toupper"、"tolower"或"isNumber"、"clipNumber"aso。
关于糟糕的性能,我经常读到:定义函数的作用域不会对性能产生任何影响,事实上,在我的FF中,作用域函数的运行速度比未定义函数快:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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( (e-s)+"ms" ); s= Date.now(); r= 0; for( i=0; i < 1000000; i++ ) { r+= fnScoped(i,i); } e= Date.now(); console.log( (e-s)+"ms" ); |
因此,在上面提到的使用WITH语句的方式中,它对性能没有负面影响,但在降低代码大小的同时,它是一个很好的方法,它会影响移动设备上的内存使用。
在许多实现中,与一起使用也会使代码速度变慢,因为现在所有内容都被包装在一个额外的查找范围中。在javascript中使用with没有正当的理由。
我认为在将模板语言转换为JavaScript时,WITH语句可以派上用场。例如,base2中的JST,但我更经常看到它。
我同意没有WITH语句就可以编程。但是因为它没有任何问题,所以它是合法的使用。
我认为对象的字面用法很有趣,就像用闭包替换掉一个
1 2 3 4 5 6 7 8 | 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]); } |
或与结束语类似的with语句
1 2 3 4 5 6 7 8 | 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语句的变量,这就是为什么我喜欢将对象文本传递给WITH,您可以确切地看到它将在代码的添加上下文中是什么。
我创建了一个"merge"函数,用
1 2 3 4 5 6 | 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; }; } |
我可以使用它类似于
用途:
1 2 3 4 5 | 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()}); } |
对于一些短代码段,我希望在度模式中使用三角函数,如
1 2 3 4 5 6 7 8 9 10 | 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 }; }; |
然后我可以在度模式下使用三角函数,而不必在
1 2 3 4 5 6 7 8 | 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; } } |
这意味着:我使用一个对象作为函数的集合,在一个有限的代码区域中启用它以进行直接访问。我觉得这个很有用。
我认为
1 2 3 | var sHeader = object.data.header.toString(); var sContent = object.data.content.toString(); var sFooter = object.data.footer.toString(); |
然后,您可以争辩说,
1 2 3 4 5 6 | var sHeader = null, sContent = null, sFooter = null; with(object.data) { sHeader = header.toString(); sContent = content.toString(); sFooter = content.toString(); } |
相反,可以说你违反了德米特定律,但是,再一次,也许不是。我离题=)。
最重要的是,要知道道格拉斯·克罗克福德建议不要使用
我只是不知道如何使用with比只键入object.member更具可读性。我不认为它的可读性差,但我也不认为它的可读性差。
正如Lassevk所说,我可以肯定地看到使用with比使用非常明确的"object.member"语法更容易出错。
它有利于将在相对复杂的环境中运行的代码放入容器中:我使用它为"window"创建本地绑定,这样就可以运行用于Web浏览器的代码。
您必须在w3schools网站http://www.w3schools.com/js/js_form_validation.asp上看到一个javascript表单的验证,在这里,对象表单被"扫描"到以查找名为"email"的输入。
但我已经修改了它,从任何表单中获取所有字段的有效性都不是空的,不管表单中字段的名称或数量如何。我只测试了文本字段。
但是with()使事情变得简单了。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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; } |
coffeeesccript的coco fork有一个
1 2 3 | with long.object.reference @a = 'foo' bar = @b |
我的
1 2 3 4 5 | 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 } |
归结为
1 2 3 4 5 | with(gapi.drive.realtime.ErrorType) {switch(e.type) { case TOKEN_REFRESH_REQUIRED: blah case CLIENT_ERROR: blah case NOT_FOUND: blah }} |
你能相信这么低质量的代码吗?不,我们看到它完全无法阅读。这个例子不可否认地证明,如果我采用可读性正确的话,就不需要WITH语句;)
对于
我有一套可以使用的瓷砖(开口朝上、朝下、朝左或朝右),我想快速添加一个瓷砖列表,在游戏开始时始终将其放置和锁定。我不想一直为列表中的每种类型输入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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"); |
使用require.js时,可以使用with来避免显式管理arity:
1 2 3 4 5 6 7 | var modules = requirejs.declare([{ 'App' : 'app/app' }]); require(modules.paths(), function() { with (modules.resolve(arguments)) { App.run(); }}); |
实施要求声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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; } } } |
正如安迪E在Shog9答案的评论中指出的那样,当使用对象文本
1 2 3 4 5 6 7 8 9 | 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]" } } |
这并不是意外行为已经不是
如果您真的还想使用这种技术,那么至少要使用一个原型为空的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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" } } |
但这只适用于ES5+。也不要使用
我正在开发一个项目,它允许用户上传代码,以便修改应用程序的某些部分的行为。在这个场景中,我一直在使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 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); |
此代码(在一定程度上)确保用户定义的代码既不能访问任何全局范围的对象,如
正如明智的说法,我仍然需要对用户提交的代码执行静态代码检查,以确保他们不会使用其他偷偷摸摸的方式访问全局范围。例如,以下用户定义的代码直接访问
1 2 3 4 | test = function() { return this.window }; return test(); |
只想添加,你可以得到"with()"功能,有漂亮的语法和没有歧义与你自己的聪明的方法…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //utility function function _with(context){ var ctx=context; this.set=function(obj){ for(x in obj){ //should add hasOwnProperty(x) here ctx[x]=obj[x]; } } return this.set; } //how calling it would look in code... _with(Hemisphere.Continent.Nation.Language.Dialect.Alphabet)({ a:"letter a", b:"letter b", c:"letter c", d:"letter a", e:"letter b", f:"letter c", // continue through whole alphabet... });//look how readable I am!!!! |
…或者如果您真的想使用"with()",而不需要含糊不清和自定义方法,请将其包装在匿名函数中,然后使用.call
1 2 3 4 5 6 7 8 9 10 11 12 13 | //imagine a deeply nested object //Hemisphere.Continent.Nation.Language.Dialect.Alphabet (function(){ with(Hemisphere.Continent.Nation.Language.Dialect.Alphabet){ this.a="letter a"; this.b="letter b"; this.c="letter c"; this.d="letter a"; this.e="letter b"; this.f="letter c"; // continue through whole alphabet... } }).call(Hemisphere.Continent.Nation.Language.Dialect.Alphabet) |
然而,正如其他人所指出的,这有点毫无意义,因为你可以做到…
1 2 3 4 5 6 7 8 9 | //imagine a deeply nested object Hemisphere.Continent.Nation.Language.Dialect.Alphabet var ltr=Hemisphere.Continent.Nation.Language.Dialect.Alphabet ltr.a="letter a"; ltr.b="letter b"; ltr.c="letter c"; ltr.d="letter a"; ltr.e="letter b"; ltr.f="letter c"; // continue through whole alphabet... |