嵌套循环中匿名闭包的可读性

时间:2011-07-09 11:59:21

标签: javascript

我的一个朋友被所有着名的'循环'javascript问题中的匿名函数所困扰。 (它被解释为死于SO,而我实际上期待有人将我的问题视为副本,这可能是公平的游戏)。

这个问题相当于John Resig在本教程中解释的内容:

http://ejohn.org/apps/learn/#62

var count = 0; 
for ( var i = 0; i < 4; i++ ) { 
  setTimeout(function(){ 
    assert( i == count++, "Check the value of i." ); 
  }, i * 200); 
}

对于一个新用户来说,它应该有用,但确实“我总是有相同的价值”,他们说,因此哭泣和牙齿咬牙切齿。

我用很多挥手解释了这个问题和一些关于范围的东西,并指出他在SO或其他网站上提供的一些解决方案(实际上,当你知道它是如此常见的问题时,google 是< / em>你的朋友)。

当然,真正的答案是在JS中,范围是在功能级别。因此,当匿名函数运行时,'i'没有在其中任何一个的范围内定义,但它在全局范围内定义,并且它具有循环结束的值,4。

由于我们很可能都接受过使用块级范围的语言的训练,所以它是另一回事 - js-that-a-little-bit-different-the-rest-of-rest-世界(“这个”的意思,任何人?)

让我感到困惑的是,共同的答案,实际上甚至是John自己提供的答案如下:

var count = 0; 
for ( var i = 0; i < 4; i++ ) (function(i){ 
   setTimeout(function(){ 
     assert( i == count++, "Check the value of i." ); 
   }, i * 200); 
})(i);

这显然有效并且证明了对语言的掌握以及对嵌套括号的一种品味,这种情况可疑地让你看起来像一个LISP-er。

然而,我不禁想到其他解决方案 更容易理解,更容易解释。

这个只是推动匿名闭包有点闭合更接近setTimeout(或更接近addEventListener是99.9%的情况下它咬人):

var count = 0;
for (var i = 0 ; i < 4 ; i++) {
    setTimeout((function (index) {
        return function () {
           assert( index == count++, "Check the value of i." ); 
        }
    })(i), i*200);
};

这是关于使用显式函数工厂解释我们正在做的事情:

var count = 0
function makeHandler(index) {
   return function() {
       assert(index == count ++);
   };
};
for (var i = 0 ; i < 4 ; i++) {
   setTimeout(makeHandler(i), i*200);
};

最后,还有另一个解决方案可以完全消除这个问题,对我来说似乎更自然(尽管我同意它以某种方式避免了'偶然'问题')

var count = 0;

function prepareTimeout(index) {
   setTimeout(function () {
      assert(index == count++);
   }, index * 200);
};

for (var k = 0; k < 4 ; k++) {
   prepareTimeout(k);
};

这些解决方案在内存使用情况,创建范围数量,可能泄漏方面完全等效吗?

对不起,如果这真的是常见问题或主观或其他。

2 个答案:

答案 0 :(得分:4)

解决方案#1

for (var i = 0; i < 4; i++) (function (i) { 
    // scope #1  
    setTimeOut(function () { 
        // scope #2
    })(i), i*200);
})(i);

实际上相当不错,因为它减少了缩进并因此降低了代码复杂性。另一方面,for循环没有自己的块,这是jsLint(在我看来是合理的)会抱怨的。

解决方案#2

for (var i = 0; i < 4; i++) {
    setTimeout((function (i) {
        // scope #1 
        return function () {
            // scope #2
        }
    })(i), i*200);
};

我大部分时间都会这样做。 for循环有一个实际的块,我发现它增加了可读性,并且与你对传统循环的期望更加合理(如“for x do y”,与“for x创建匿名函数并立即执行”来自#1)。但这是在旁观者的眼中,真的,你拥有的经验越多,这些方法开始对你来说就越相同。

解决方案#3

function makeHandler(index) {
   // scope #1 
   return function() {
       // scope #2
   };
};
for (var i = 0 ; i < 4 ; i++) {
   setTimeout(makeHandler(i), i*200);
}; 

正如你所说的那样,for循环本身更清晰。人类思维可以更容易地适应命名块,这些块做预定义的事情而不是一堆嵌套的匿名函数。

function prepareTimeout(index) {
   // scope #1
   setTimeout(function () {
       // scope #2
   }, index * 200);
};

for (var k = 0; k < 4 ; k++) {
   prepareTimeout(k);
};

绝对是一回事。我在这里看不到任何“问题回避”,它只相当于#3。

正如我所看到的,这些方法在最低限度上没有区别,语义 - 只有语法。有时候有理由偏好一个(例如“功能工厂”方法非常可重用),但这并不适用于你描述的标准情况。

无论如何,JavaScript的新用户必须掌握三个概念:

  1. 函数是对象,可以像整数一样传递(而因此它们不需要名称)
  2. 功能范围和范围保存(关闭如何工作)
  3. asynchronicity如何运作
  4. 一旦这些概念陷入困境,你就不会再看到这些方法有如此大的差异。他们只是“把它”的不同方式。在那之前,你只需选择一个你最熟悉的那个。


    编辑:你可以争辩说,当你从过渡时,你必须像这样“态度转变为”你可以这样做,或者像这样,或者喜欢这种“态度。这适用于编程语言,烹饪和其他任何东西。

    要谈谈标题中的隐含问题:可读性也是旁观者的眼睛。问任何Perl程序员。或者对正则表达式感到满意的人。

答案 1 :(得分:0)

在我看来,最后一个模式(使用预定义的命名函数)是最具可读性的“可调试”,并且在我的编辑器中最常用(KomodoEdit或Visual Studio与Resharper 6.0结合使用),这很容易从函数调用跳转到函数定义。它只是在编码时要求更多的纪律。