如何解决ActionScript 3(AS3)中的闭包问题

25
在下面的代码中,我试图加载一些图像,并在它们被单独加载后立即将它们放在舞台上。但是有一个bug,只有最后一个图像被显示。我怀疑这是闭包问题。我该如何解决?AS3中闭包的行为不是与JavaScript相同吗?
var imageList:Array = new Array();
imageList.push({'src':'image1.jpg'});
imageList.push({'src':'image2.jpg'});
var imagePanel:MovieClip = new MovieClip();
this.addChildAt(imagePanel, 0);

for (var i in imageList) {
    var imageData = imageList[i];
    imageData.loader = new Loader();

    imageData.loader.contentLoaderInfo.addEventListener(
        Event.COMPLETE, 
        function() {
            imagePanel.addChild(imageData.loader.content as Bitmap);
            trace('Completed: ' + imageData.src);             
        });

    trace('Starting: ' + imageData.src);
    imageData.loader.load(new URLRequest(imageData.src));   
}

我不建议在与主题关联不大时使用javascript标签,因此我将其移除。 - Salty
4个回答

45
闭包在AS3中的行为与JavaScript相同吗?
是的,JavaScript确实和其他一些语言(如Python)完全一样。尽管您在“for”循环内定义了“var imageData”,但这些语言中的for循环并不引入新作用域;实际上,变量imageData绑定在包含作用域中(外部函数或者在这种情况下似乎是全局作用域)。当循环执行完成后,您可以查看imageData,并在其中找到imageList的最后一个元素。
因此,只有一个imageData变量,而不是每个循环迭代一个。当COMPLETE触发时,它进入闭包并读取imageData当前的值,而不是在定义函数时的值(* )。通常,在COMPLETE触发时,for循环已经完成,而imageData将保留最后一个迭代中的那个元素。
(* - 存在“早期绑定”语言,它们会在定义闭包时评估变量的值。但是ActionScript不是其中之一。)可能的解决方案往往涉及使用外部函数来引入新作用域。例如:
function makeCallback(imageData) { return function() {
    imagePanel.addChild(imageData.loader.content as Bitmap);
    trace('Completed: ' + imageData.src);                                                                                                     
} }
...
imageData.loader.contentLoaderInfo.addEventListener(Event.COMPLETE, makeCallback(imageData));

你/可以/内联放置它,但是双重嵌套的function()开始变得难以阅读。

另请参见Function.bind(),它是一个通用的部分函数应用功能,可用于实现此目的。它可能是未来JavaScript / ActionScript版本的一部分,并且在此期间可以通过原型添加到语言中。


感谢您澄清事情。实际上,行为与JavaScript不同,因为JavaScript是一种“早期绑定”语言,我认为这更好。为什么有人会喜欢AS3的行为呢? - lbrandao
不,ActionScript和JavaScript的行为是相同的 - 它们必须如此,因为它们都符合ECMAScript标准。它们都不是早期绑定语言,这种语言在函数式编程中更常见。我很想看到一种基于现代脚本语言的早期绑定语言。 - bobince
你说得对!我进行了一些测试,事实上行为是相同的。我想我认为它不同是因为我从未遇到过JavaScript中这种延迟的情况。谢谢! - lbrandao
JQuery是否在某种程度上改变了这种行为?我经常使用它,也许它让我感到困惑。 - lbrandao
2
不是固有的,但 jQuery 的 sequence.Each() 在每次迭代中都会调用一个函数,并重新绑定函数变量。因此,它避免了 for/while 循环可能遇到的问题。 - bobince
这是一个很棒的答案。希望我能给它点赞一百次以上! - PEZ

3
使用Array类上的更具功能性的forEach方法可以避免这个问题。虽然这已经提到过了,但我在这里会进一步扩展说明。
imageList.forEach( function ( item:MovieClip, index:int, list:Array) {
    // add your listener with closure here
})

使用这种方法,传递到forEach中的函数会在每次迭代时定义一个新的作用域。现在你可以在这个作用域内添加闭包,并且它将按照你想要的方式记住每个实例。
另外需要注意的是:
每次都输入那3个参数很麻烦,所以... 你也可以使用适配器函数来使其更简洁 / 更优雅。
// just give me the item please
imageList.forEach ( itrAdpt( function ( image: ImageData ) {
    // add your listener with closure here
}))

// give me the item and it's index
imageList.forEach ( itrAdpt( function ( image: ImageData, index:int ) {
    // add your listener with closure here
}))

// give me the item, index and total list length
imageList.forEach ( itrAdpt( function ( image: ImageData, index:int, length:int ) {
    // add your listener with closure here
}))

其中itrAdpt是一个函数,可能是全局函数,定义如下:

public function itrAdpt(f: Function): Function
{
    var argAmount:int = f.length

    if (argAmount == 0)
    {
        return function (element:*, index:int, colection:*):* {
            return f(element)
        }
    }
    else if (argAmount == 1)
    {
        return function (element:*, index:int, colection:*):* {
            return f(element)
        }
    }
    else if (argAmount == 2)
    {
        return function (element:*, index:int, colection:*):* {
            return f(element, index)
        }
    }
    else if (argAmount == 3)
    {
        return function (element:*, index:int, colection:*):* {
            return f(element, index, colection.length)
        }
    }
    else
    {
        throw new Error("Don't know what to do with "+argAmount+"arguments. Supplied function should have between 1-3 arguments")
    }
}

1
如果你不喜欢创建一个命名函数,那么bobince的答案可以转换成这个形式,而不会牺牲可读性太多:
var makeCallback = function(imageData:String) 
  { 
    return function(evt:Event) 
    {
      imagePanel.addChild(imageData.loader.content as Bitmap);
      trace('Completed: ' +  imageData.src);
    } 
  }

...

imageData.loader.contentLoaderInfo.addEventListener(Event.COMPLETE, makeCallback(imageData));

这只是我的个人偏好,你的情况可能会有所不同。


1
(function() {
  var imageData = imageList[i];
  imageData.loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function() {
    // use imageData;
  });
}).apply();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接