2012年2月27日星期一

你必须知道的【闭包】陷阱和案例

闭包

In some languages, a closure may occur when a function is defined within another function, and the inner function refers to local variables of the outer function.

At run-time, when the outer function executes, a closure is formed, consisting of the inner function’s code and references (the upvalues) to any variables of the outer function required by the closure.

 

翻译:

在一些编程语言当中,闭包发生  :在一个函数内部定义了另外一个函数,并且内部的函数引用了外部函数的本地变量。

在运行的时候,当外部函数执行,这个时候形成了一个闭包,由内部函数的代码和对外部函数任意变量引用组成,这写引用都依赖于此闭包。

// ECMAScript 
var f, g; 
function foo() { 
    var x = 0; 
    f = function () { return ++x; }; 
    g = function () { return --x; }; 
    x = 1; 
    alert('inside foo, call to f(): ' + f()); // "2" 
} 
//外部函数执行,这个时候形成了闭包
foo(); 
//因为有了闭包,所以才访问到了 foo中的x 
alert('call to g(): ' + g()); // "1" 
//因为有了闭包,所以才访问到了 foo中的x 
alert('call to f(): ' + f()); // "2" 

 

 

javascript闭包陷阱与案例

在很多时候,由于内部函数的变量覆盖了闭包的变量,我们如果需要引用外部函数同名的变量,需要通过执行匿名函数,不外部函数的变量作为参数传递进来。如下所示:

(function(out_xxx){
//这里面就可以使用out_xxx
 }(xxx))

可以看得出来,使用这种方式最多的地方是在我们定义对象的时候:

(function (window) {
    var MyObject = function () {
        this.initialize();
    }
    var p = DisplayObject.prototype;
    p.initialize = function () { 
    
    }
    window.MyObject = MyObject;
} (window));

这样定义对象有两个好处:

1.避免污染外部变量

2.传递参数的形式减少作用域查找

javascript为我们埋了很多坑,在许多场景下,我们需要利用以上的形式去解决问题,下面依依列出。

 

场景1 :

如下所示,我需要在cc方法中调用到外面的name:

    var bb, cc;
    function aa() {
        var name = "当耐特";
        bb = function () {
            var name = "砖家";
            cc = function () {
                var name = "张磊";
                alert(name);
            }
        }
    }
        aa();
        bb();
        cc();//输出 “张磊”
因为内部的函数定义的变量覆盖了外部函数的变量,所以结果输出“张磊”。

解决办法:

    var bb, cc;
    function aa() {
        var name = "当耐特";
        (function (aa_name) {
            bb = function () {
                var name = "砖家";
                (function (bb_name, aa_name) {
                    cc = function () {
                        var name = "张磊";
                        alert(aa_name);
                        alert(bb_name);
                        alert(name);
                    }
                })(name, aa_name);
            }
        })(name);
    }
        aa();
        bb();
        cc();//输出“当耐特” “砖家”  “张磊”

 

真实案例:

记得上周,我的一个同事(实习生),对下面一段代码产生疑惑,所以咨询我。如下所示:

    $("#dialog-form").dialog({
        autoOpen: false,
        height: 300,
        width: 350,
        modal: true,
        buttons: {
            "Create an account": function () {
                var bValid = true;
                allFields.removeClass("ui-state-error");
                bValid = bValid && checkLength(name, "username", 3, 16);
                bValid = bValid && checkLength(email, "email", 6, 80);
                bValid = bValid && checkLength(password, "password", 5, 16);
                if (bValid) {
                    $.ajax({
                        type: "POST",
                        url: "xxxxx.aspx",
                        data: "name=xxxxx&email=xxxxx&password=xxxx"
                    }).done(function (msg) {
                        alert("Data Saved: " + msg);
                        $(this).dialog("close");
                    });
                }
            },
            Cancel: function () {
                $(this).dialog("close");
            }
        },
        close: function () {
            allFields.val("").removeClass("ui-state-error");
        }
    });

这里用的是JqueryUI的dialog插件。详见: http://jqueryui.com/demos/dialog/#modal-form  

image

 

他想要的效果是点击create发起一个异步提交,然后在回调的时候关闭弹出层。令他困惑的地方是,弹出层关闭不了。

他抱怨着说:

Cancel: function () {
                $(this).dialog("close");
            }

我的cancel都能关闭。为什么

                if (bValid) {
                    $.ajax({
                        type: "POST",
                        url: "xxxxx.aspx",
                        data: "name=xxxxx&email=xxxxx&password=xxxx"
                    }).done(function (msg) {
                        alert("Data Saved: " + msg);
                        $(this).dialog("close");
                    });
                }

这里面的$(this).dialog("close")为什么就不能关闭?
这是一个很典型的场景,解决办法:

                if (bValid) {
                    (function (outThis) {
                        $.ajax({
                            type: "POST",
                            url: "xxxxx.aspx",
                            data: "name=xxxxx&email=xxxxx&password=xxxx"
                        }).done(function (msg) {
                            alert("Data Saved: " + msg);
                            $(outThis).dialog("close");
                        });
                    } 
                }(this))
            },

 

场景2---循环中的内部函数

    function TestObj(name) {
        this.name = name;
    }
    var objs = [];
    var obj;
    function test() {
        for (var i = 0; i < 100; i++) {
            var name = "张磊" + i;
            obj = new TestObj(name);
            obj.printName = function () {
                    console.log(obj.name);
                }
            objs.push(obj);
        }
    }
    //外部函数执行,闭包形成。内部函数obj.printName中的obj全部指向最后一次new TestObj(name);
    test();
    //所以这里会输出100次-----"张磊99"
    for (var i in objs) {
        objs[i].printName();
    }

解决办法

    function TestObj(name) {
        this.name = name;
    }
    var objs = [];
    var obj;
    function test() {
        for (var i = 0; i < 100; i++) {
            var name = "张磊" + i;
            obj = new TestObj(name);
              (function (target) {
            obj.printName = function () {
                console.log(target.name);
            }
               } (obj))
            objs.push(obj);
        }
    }
    test();
    for (var i in objs) {
        objs[i].printName();
    }

 

真实案例:

// create and populate the screen with random daisies: 
for(var i = 0; i < 100; i++){ 
    bitmap = new Bitmap(image); 
    container.addChild(bitmap); 
    bitmap.x = canvas.width * Math.random()|0; 
    bitmap.y = canvas.height * Math.random()|0; 
    bitmap.rotation = 360 * Math.random()|0; 
    bitmap.regX = bitmap.image.width/2|0; 
    bitmap.regY = bitmap.image.height/2|0; 
    bitmap.scaleX = bitmap.scaleY = bitmap.scale = Math.random()*0.4+0.6; 
    bitmap.name = "bmp_"+i;
    // wrapper function to provide scope for the event handlers: 
    (function(target) { 
        bitmap.onPress = function(evt) { 
            // bump the target in front of it's siblings: 
            container.addChild(target); 
            var offset = {x:target.x-evt.stageX, y:target.y-evt.stageY};
            // add a handler to the event object's onMouseMove callback 
            // this will be active until the user releases the mouse button: 
            evt.onMouseMove = function(ev) { 
                target.x = ev.stageX+offset.x; 
                target.y = ev.stageY+offset.y; 
                // indicate that the stage should be updated on the next tick: 
                update = true; 
            } 
        } 
        bitmap.onMouseOver = function() { 
            target.scaleX = target.scaleY = target.scale*1.2; 
            update = true; 
        } 
        bitmap.onMouseOut = function() { 
            target.scaleX = target.scaleY = target.scale; 
            update = true; 
        } 
    })(bitmap); 
}

这是Easeljs官网demo的一段代码,因为内部函数不会立即执行,所以当执行的时候,内部函数引用外部函数变量的时候,该变量已经被外层的for循环覆盖了N次,所以要通过上面的方式来解决这个问题。

 

小结

这是javascript蹩脚的一个方面,除了这个,还有javascript的 getter和setter也是该语言语法特性中令人憋屈的地方。

我相信经过人类的不懈努力,总有那么一天:static 、namespace、interface、private、protected 、class、enum·············都能成为javascript的关键字,而不是五花八门的技巧。相信那个时候,对应这些关键字的所有 文章 笔记  心得  日记 技巧 随笔 后门  都将沉尸谷底、永无翻身之日。


你必须知道的【闭包】陷阱和案例

TAG: