阿兰·斯托姆对我关于with声明的回答的评论让我思考起来。我很少找到使用这个特定语言特性的理由,也从未考虑过它会如何引起麻烦。现在,我很好奇如何有效地利用with,同时避免它的陷阱。
你在哪里发现with语句有用?
今天我又遇到了另一种用法,所以我兴奋地搜索了一下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循环没有引入新的作用域,所以这三个函数将共享同一个num,值为2。
新范围:
let和
with。
随着ES6中引入了let语句,在必要时可以很容易地引入一个新的范围来避免这些问题:
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普及之前,这种使用仍然局限于最新的浏览器和愿意使用蒸腾器的开发人员。但是,我们可以使用with很容易地模拟这种行为:
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浏览器中引入的let块语法,但在其他地方没有广泛采用。
我一直将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')
)
)
} |
对于这个用例,我不做任何分配,所以我没有与之相关的模糊性问题。
正如我之前的评论所指出的,无论在任何给定的情况下有多诱人,我认为你都不能安全地使用With。既然这个问题不在这里直接讨论,我再重复一遍。考虑以下代码
1 2 3 4 5 6 7 8
| 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语句将作用域"分层"到全局作用域之后:
1 2 3 4 5
| with (consoleCommands) {
with (window) {
eval(expression);
}
} |
这项技术的好处在于,除了性能方面的缺点外,它不会受到with声明通常的担心,因为我们无论如何都在全局范围内进行评估-我们的伪范围之外的变量没有被修改的危险。
令我惊讶的是,当我发现其他地方也使用了同样的技术——铬源代码时,我被激励去发布这个答案。
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"; |
等。。。
您可以定义一个小的助手函数来提供With的好处,而不需要含糊不清:
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"; |
我从来没有用过,没有理由,也不推荐。
with的问题在于它阻止了ECMAScript实现可以执行的大量词汇优化。考虑到基于快速JIT的引擎的兴起,这个问题在不久的将来可能会变得更加重要。
它可能看起来像with允许更干净的构造(比如,当引入一个新的作用域而不是一个通用的匿名函数包装器或替换冗长的别名时),但它确实不值得。除了性能下降之外,总是存在分配给错误对象的属性的危险(当在注入范围内的对象上找不到属性时),并且可能错误地引入全局变量。IIRC,后一个问题是促使Crockford建议避免with。
Visual Basic.NET有一个类似的With语句。我使用它的一个更常见的方法是快速设置一些属性。而不是:
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 |
前者是someObject.Foo;后者是Foo在someObject以外的范围内。
我发现JavaScript缺乏区分性使得它远不如VisualBasic的变体有用,因为模棱两可的风险太高。除此之外,With仍然是一个强大的思想,可以使可读性更好。
您可以使用with将对象的内容作为局部变量引入到块中,就像使用这个小模板引擎一样。
使用"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"函数,用with语句消除了这种模糊性:
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;
};
} |
我可以使用它类似于with,但我知道它不会影响我不打算影响的任何范围。
用途:
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()});
} |
对于一些短代码段,我希望在度模式中使用三角函数,如sin、cos等,而不是辐射模式。为此,我使用了AngularDegree对象:
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 };
}; |
然后我可以在度模式下使用三角函数,而不必在with块中使用进一步的语言噪声:
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;
}
} |
这意味着:我使用一个对象作为函数的集合,在一个有限的代码区域中启用它以进行直接访问。我觉得这个很有用。
我认为with的有用性取决于代码编写的好坏。例如,如果编写的代码如下所示:
1 2 3
| var sHeader = object.data.header.toString();
var sContent = object.data.content.toString();
var sFooter = object.data.footer.toString(); |
然后,您可以争辩说,with将通过这样做来提高代码的可读性:
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。我劝你看看他关于with及其替代品的博客文章。
我只是不知道如何使用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有一个with关键字,但它只将this设置为块内的目标对象(也可写为coffeeesccript/coco中的@。这消除了歧义,实现了ES5严格的模式遵从性:
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语句;)
对于with有一个很好的用途:根据存储在该对象中的值,向对象文本添加新元素。下面是我今天刚刚使用的一个示例:
我有一套可以使用的瓷砖(开口朝上、朝下、朝左或朝右),我想快速添加一个瓷砖列表,在游戏开始时始终将其放置和锁定。我不想一直为列表中的每种类型输入types.tbr,所以我只是使用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答案的评论中指出的那样,当使用对象文本with时,会发生这种潜在的意外行为:
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]"
}
} |
这并不是意外行为已经不是with的标志。
如果您真的还想使用这种技术,那么至少要使用一个原型为空的对象。
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+。也不要使用with。
我正在开发一个项目,它允许用户上传代码,以便修改应用程序的某些部分的行为。在这个场景中,我一直在使用with子句来防止他们的代码修改超出我希望他们处理的范围之外的任何东西。我用来执行此操作的代码的(简化)部分是:
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); |
此代码(在一定程度上)确保用户定义的代码既不能访问任何全局范围的对象,如window,也不能通过闭包访问任何局部变量。
正如明智的说法,我仍然需要对用户提交的代码执行静态代码检查,以确保他们不会使用其他偷偷摸摸的方式访问全局范围。例如,以下用户定义的代码直接访问window:
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... |