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

艾伦 · 斯托姆对我关于 with语句的回答的评论让我思考。我很少找到使用这种特殊语言特性的理由,也从未想过它会带来什么麻烦。现在,我很好奇如何有效地利用 with,同时避免它的陷阱。

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

70340 次浏览

Visual Basic。NET有类似的With语句。我使用它的一个比较常见的方法是快速设置一些属性。而不是:

someObject.Foo = ''
someObject.Bar = ''
someObject.Baz = ''

,我可以这样写:

With someObject
.Foo = ''
.Bar = ''
.Baz = ''
End With

这不仅仅是懒惰的问题。它还使代码可读性更强。与JavaScript不同的是,它不会有歧义,因为你必须在所有受语句影响的东西前面加上.(点)。所以,下面两个是明显不同的:

With someObject
.Foo = ''
End With

vs。

With someObject
Foo = ''
End With

前者是someObject.Foo;后者是 someObject作用域中的Foo

我发现JavaScript缺乏区分使得它远不如Visual Basic的变体有用,因为歧义的风险太高了。除此之外,with仍然是一个强大的思想,可以提高可读性。

我认为最明显的用途是作为捷径。例如,如果你初始化一个对象,你只需节省大量输入“ObjectName”。有点像lisp的“带槽”,让你可以写

(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”时,这是一个更明显的快捷方式。但是还是。

根据使用Delphi的经验,我会说使用应该是最后的大小优化方法,可能由某种javascript最小化算法执行,并访问静态代码分析以验证其安全性。

随意使用语句可能会陷入范围问题,这可能是一个非常痛苦的问题,我不希望任何人经历调试会话来弄清楚他…,却发现它捕获了一个对象成员或错误的局部变量,而不是您期望的全局变量或外部作用域变量。

VB的语句更好,因为它需要点来消除范围的歧义,但Delphi的语句是一把上膛的枪,带有一触发发的扳机,在我看来,似乎javascript的语句是足够相似的,以保证相同的警告。

你可以定义一个小的帮助函数来提供with的好处,而不会产生歧义:

var with_ = function (obj, func) { func (obj); };


with_ (object_name_here, function (_)
{
_.a = "foo";
_.b = "bar";
});

我只是不明白使用with比直接输入object。member有什么更好读的地方。我不认为它的可读性有任何下降,但我也不认为它的可读性有任何提高。

就像lassevk说的,我可以肯定地看到使用with比使用非常显式的“object”更容易出错。成员”语法。

使用with还会使代码在许多实现中变慢,因为现在所有内容都被包装在一个额外的查找范围中。在JavaScript中使用with没有合理的理由。

正如我之前的评论所指出的,我不认为你可以安全地使用with,无论它在任何给定的情况下有多么诱人。由于这里没有直接涉及到这个问题,我将重复一遍。考虑以下代码

user = {};
someFunctionThatDoesStuffToUser(user);
someOtherFunction(user);


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

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

错误发生。如果你使用,你最终会这样做,并增加你的程序失败的机会。更糟糕的是,您可能会遇到在with块中设置全局变量的工作代码,或者是故意的,或者是作者不知道这个构造的怪处。这很像在开关上遇到了故障,你不知道作者是否有意这样做,也没有办法知道“修复”代码是否会引入回归。

现代编程语言充满了特性。一些特性,在使用多年后,被发现是不好的,应该避免使用。Javascript的with就是其中之一。

似乎不值得,因为你可以做到以下几点:

var o = incrediblyLongObjectNameThatNoOneWouldUse;
o.name = "Bob";
o.age = "50";

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

我同意不使用with语句就可以编程。但因为它不会带来任何问题,所以它是一种合法的使用。

我认为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();
}

相反,你可能会认为你违反了得墨忒耳定律,但是,也许不是。我跑题了。

最重要的是,要知道Douglas Crockford推荐使用with。我强烈建议你看看他的博客文章关于with和它的替代品在这里

今天我想到了另一个用法,所以我兴奋地在网上搜索,发现了一个现有的提及:在块作用域内定义变量

背景

尽管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)将被所有三个函数共享。

一个新的作用域:letwith

通过在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 (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";

等等……

我一直使用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的问题是ECMAScript实现可以执行防止大量词汇优化。随着基于jit的快速引擎的兴起,这个问题在不久的将来可能会变得更加重要。

它可能看起来像with允许更清晰的结构(当,比如,引入一个新的作用域而不是公共的匿名函数包装器或替换冗长的别名时),但它是真的不值得。除了性能下降之外,还存在给错误对象的属性赋值的危险(当在注入作用域中的对象上找不到属性时),以及可能错误地引入全局变量。IIRC,后一个问题是促使Crockford建议避免with的原因。

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

// 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”麻生..

关于糟糕的性能,我经常读到:作用域函数不会对性能产生任何影响,事实上,在我的FF中,有作用域的函数比无作用域的函数运行得更快:

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语句对性能没有负面影响,但它减少了代码大小,这影响了移动设备上的内存使用。

它很适合将在相对复杂的环境中运行的代码放入容器中:我使用它来为“窗口”创建本地绑定,从而运行用于web浏览器的代码。

我认为对象文字的使用很有趣,就像使用闭包的一个插入式替换

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语句

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,你可以确切地看到它将在代码中添加的上下文中。

使用“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的人会选择后者。

我创建了一个“merge”函数,用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;
};
}

我可以类似地使用with,但我可以知道它不会影响我不打算让它影响的任何范围。

用法:

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

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

你可以在W3schools http://www.w3schools.com/js/js_form_validation.asp中看到javascript表单的验证,其中对象表单被“扫描”以找到名称为“email”的输入

但我已经修改了它,从任何形式的所有字段验证为不空,无论名称或数量的字段在一个形式。我只测试了文本字段。

但是with()使事情变得更简单。代码如下:

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

实际上,我最近发现with语句非常有用。直到我开始了我目前的项目——一个用JavaScript编写的命令行控制台,我才真正意识到这个技术。我试图模拟Firebug/WebKit控制台api,其中特殊命令可以输入到控制台,但它们不会覆盖全局作用域中的任何变量。当我试图克服我在Shog9的回答非常棒的评论中提到的一个问题时,我想到了这一点。

为了达到这个效果,我使用了两个with语句将一个作用域“分层”到全局作用域后面:

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

这种技术的优点在于,除了性能方面的缺点外,它不会遭受with语句通常带来的恐惧,因为我们无论如何都是在全局作用域中求值——伪作用域之外的变量不会被修改。

让我惊讶的是,当我设法找到在其他地方使用的相同技巧时,我受到了启发,发布了这个答案——铬源代码!

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

CoffeeScript的椰子树分支有一个with关键字,但它只是将this(在CoffeeScript/Coco中也可写为@)设置为块中的目标对象。这消除了歧义,实现了ES5严格模式兼容:

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

对于一些简短的代码片段,我想在度模式而不是辐射模式中使用sincos等三角函数。为此,我使用AngularDegreeobject:

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块中以度模式使用三角函数,而没有进一步的语言噪声:

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,在ECMAScript 5严格模式下禁止使用。推荐的替代方法是将您想要访问其属性的对象分配给一个临时变量。

来源:Mozilla.org

下面是with的一个很好的用法:根据存储在对象中的值向对象Literal添加新元素。这是我今天用的一个例子:

我有一组可能使用的瓷砖(面向顶部、底部、左侧或右侧),我想快速添加一个瓷砖列表,在游戏开始时始终放置并锁定这些瓷砖。我不想为列表中的每个类型都输入types.tbr,所以我只使用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");

你可以使用with来避免在使用require.js时显式地管理arity:

var modules = requirejs.declare([{
'App' : 'app/app'
}]);


require(modules.paths(), function() { with (modules.resolve(arguments)) {
App.run();
}});

requirews .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;
}
}
}

正如Andy E在Shog9的回答的评论中指出的那样,在使用with和对象字面值时,会发生这种潜在的意外行为:

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的标志。

如果您仍然想使用这种技术,至少使用一个具有空原型的对象。

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子句来防止他们的代码修改我想让他们瞎折腾的范围之外的任何东西。我用来做这件事的(简化)代码部分是:

// 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的直接访问:

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

只是想添加你可以得到“with()”功能与漂亮的语法和没有歧义与你自己的聪明的方法…

     //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

//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)

然而,正如其他人所指出的,这有点毫无意义,因为你可以做……

 //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...

我的

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
}

归结起来就是

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

你能相信这么低质量的代码吗?不,我们看到它是完全不可读的。这个例子无可否认地证明了没有必要使用with-statement,如果我的可读性是正确的;)

使用“with"带有代理对象的语句

我最近想为巴别塔写一个启用宏的插件。我想有一个单独的变量名称空间来保存我的宏变量,我可以在这个空间中运行我的宏代码。此外,我想检测宏代码中定义的新变量(因为它们是新的宏)。

首先,我选择了虚拟机模块,但我发现虚拟机模块中的全局变量,如数组,对象等与主程序不同,我不能实现与全局对象完全兼容的modulerequire(因为我不能重构核心模块)。最后,我发现"with"声明。

const runInContext = function(code, context) {
context.global = context;
const proxyOfContext = new Proxy(context, { has: () => true });
let run = new Function(
"proxyOfContext",
`
with(proxyOfContext){
with(global){
${code}
}
}
`
);
return run(proxyOfContext);
};

这个代理对象捕获所有变量的搜索,并说:“是的,我有那个变量。”如果代理对象实际上没有该变量,则将其值显示为undefined

这样,如果在宏code中使用var语句定义了任何变量,我就可以在上下文对象中找到它(比如vm模块)。但是用letconst定义的变量仅在该时间内可用,并且不会保存在上下文对象中(vm模块保存它们但不公开它们)。

性能:此方法的性能优于vm.runInContext

安全:如果你想在沙箱中运行代码,这无论如何都是不安全的,你必须使用vm模块。它只提供一个新的名称空间。

当你需要将对象结构从平面转换为层次结构时,with与简略的对象符号结合使用是非常有用的。如果你有:

var a = {id: 123, name: 'abc', attr1: 'efg', attr2: 'zxvc', attr3: '4321'};

所以不要:

var b = {
id: a.id,
name: a.name
metadata: {name: a.name, attr1: a.attr1}
extrastuff: {attr2: a.attr2, attr3: a.attr3}
}

你可以简单地写:

with (a) {
var b = {
id,
name,
metadata: {name, attr1}
extrastuff: {attr2, attr3}
}
}