JavaScript的" with"语句有合法用途吗?

时间:2020-03-05 18:53:03  来源:igfitidea点击:

艾伦·斯托姆(Alan Storm)对我对" with"声明的回答引起的评论使我开始思考。我很少找到使用此特定语言功能的理由,并且从未考虑过如何可能引起麻烦。现在,我很好奇如何有效地使用with,同时又避免了陷阱。

我们在哪里发现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";后者是someObject范围之外的Foo。

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

回答

我认为明显的用途是作为捷径。如果我们是初始化一个对象,我们只需输入很多" 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语句是一头装满了扳机的枪,在我看来,JavaScript脚本足够相似,可以发出相同的警告。

回答

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

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

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

回答

我只是真的看不到使用with而不是仅仅键入object.member更具可读性。我不认为它的可读性较差,但我也不认为它的可读性也较高。

就像lassevk所说的,我绝对可以看到使用with会比仅使用非常明确的" object.member"语法更容易出错。

回答

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

回答

正如我之前的评论所指出的那样,无论在任何给定情况下多么诱人,我都不认为我们可以安全地使用with。由于此问题未在此处直接涉及,因此我将重复进行。考虑以下代码

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

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

如果不仔细研究这些函数调用,就无法判断该代码运行后程序的状态。如果已经设置了" user.name",那么它将是" Bob"。如果未设置,则全局"名称"将被初始化或者更改为"鲍勃",并且"用户"对象将保持不具有"名称"属性。

发生错误。如果与我们一起使用,最终将这样做并增加程序失败的机会。更糟糕的是,我们可能会故意地或者通过作者不知道构造的怪癖的情况而遇到在with块中设置全局设置的工作代码。这很像遇到切换失败,我们不知道作者是否打算这样做,也无法知道"修复"代码是否会引入回归。

现代编程语言充斥着各种功能。使用多年后,发现某些功能不好,应避免使用。 Javascript的" with"就是其中之一。

回答

似乎不值得,因为我们可以执行以下操作:

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

回答

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

我同意可以在没有声明的情况下对此进行编程。但是因为它没有任何问题,所以是合法使用。

回答

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

相反,可以说我们违反了得墨meter耳定律,但也许没有。我离题=)。

最重要的是,请知道道格拉斯·克罗克福德(Douglas Crockford)建议不要使用with。我敦促我们在此处查看有关with及其替代方法的博客文章。

回答

今天,我想到了另一种用法,因此我兴奋地搜索了网络,并发现了现有的内容:在Block Scope中定义变量。

背景

尽管JavaScript与C和C ++表面上很相似,但它们并未将变量的作用域限定在以下变量中:

var name = "Joe";
if ( true )
{
   var name = "Hyman";
}
// name now contains "Hyman"

在循环中声明闭包是一项常见的任务,它可能导致错误:

for (var i=0; i<3; ++i)
{
   var num = i;
   setTimeout(function() { alert(num); }, 10);
}

因为for循环没有引入新的作用域,所以所有三个函数将共享值" 2"的相同" num"。

新的作用域: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"可以使代码更加干燥。

考虑以下代码:

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或者Cwould的人会选择第一种方式(object.member),而喜欢Ruby或者Python的人会选择后者。

回答

我创建了一个"合并"函数,该函数通过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语句的恐惧,因为无论如何我们在全局范围内进行评估,在伪作用域之外没有变量的危险从被修改。

令我惊讶的是,当我设法找到在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);" +
    "}" +
"}}}}";