濃縮解讀《JavaScript設(shè)計(jì)模式與開(kāi)發(fā)實(shí)踐》③

三、閉包和高階函數(shù)

IMG_20170112_004128.jpg

3.1 閉包

3.1.1 變量的作用域

  • 所謂變量的作用域,就是變量的有效范圍。通過(guò)作用域的劃分,JavaScript變量分為全局變量和局部變量。
  • 聲明在函數(shù)外的變量為全局變量;在函數(shù)內(nèi)并且以var關(guān)鍵字聲明的變量為局部變量
  • 我們都知道,全局變量能在任何作用域訪問(wèn)到,但這很容易造成命名沖突;而局部變量只有在函數(shù)里面能訪問(wèn)到,這是因?yàn)镴avaScript的查找變量的規(guī)則是從內(nèi)往外搜索的。

3.1.2 變量的生命周期

  • 全局變量的生命周期是永久的(除非我們主動(dòng)銷(xiāo)毀這個(gè)全局變量),而局部變量則當(dāng)函數(shù)執(zhí)行完畢時(shí)被銷(xiāo)毀。
  • 那JavaScript中是否存在,即便函數(shù)執(zhí)行完畢,依然不會(huì)被銷(xiāo)毀的局部變量?答案是肯定的。
<script type="text/javascript">
    //現(xiàn)在有一個(gè)名為func的函數(shù)
    var func = function(){
        //①函數(shù)執(zhí)行體中,將局部變量a賦值為1
        var a = 1;
        //②返回一個(gè)function執(zhí)行環(huán)境
        return function(){
            //③執(zhí)行環(huán)境中,將func.a局部變量加1,然后輸出到控制臺(tái)
            a++;
            console.info(a);
        }
    };
    
    //調(diào)用:將func函數(shù)執(zhí)行后的返回,賦值給f
    var f = func();
    f();    //f()調(diào)用一次,輸出2
    f();    //f()再調(diào)用一次,輸出3
    f();    //f()接著調(diào)用,輸出4
</script>
  • 以上案例中的func.a局部變量,在func()函數(shù)執(zhí)行過(guò)后并沒(méi)有被銷(xiāo)毀。每次執(zhí)行f()時(shí),仍能對(duì)它進(jìn)行累加,就是佐證。這是因?yàn)?code>func()返回了一個(gè)匿名函數(shù)的引用賦值給f,正是由于被外部變量引用了,所以不被銷(xiāo)毀,此時(shí)這個(gè)匿名函數(shù)就稱為閉包
  • 什么是閉包?

    在一個(gè)函數(shù)內(nèi)定義另外一個(gè)函數(shù)(內(nèi)部函數(shù)可以訪問(wèn)外部函數(shù)的變量),如果將這個(gè)內(nèi)部函數(shù)提供給其他變量引用時(shí),內(nèi)部函數(shù)作用域以及依賴的外部作用域的執(zhí)行環(huán)境就不會(huì)被銷(xiāo)毀。此時(shí)這個(gè)內(nèi)部函數(shù)就像一個(gè)可以訪問(wèn)封閉數(shù)據(jù)包的執(zhí)行環(huán)境,也就是閉包。

3.1.3 閉包的用途

  • 我們不但要學(xué)習(xí)什么是JavaScript閉包,更要了解如何利用閉包特性來(lái)寫(xiě)代碼。由于篇幅有限,書(shū)中只羅列了幾個(gè)使用閉包的例子,但要知道實(shí)際開(kāi)發(fā)中運(yùn)用閉包非常廣泛,遠(yuǎn)不止于此。
  1. 封裝變量:通過(guò)閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
    var name = "William";
    return function(){
        console.info(name);          
    };
})();
person();   // 輸出成功
console.info(person.name);  //// 輸出失敗
  1. 延續(xù)變量的生命周期:我們經(jīng)常用<img>標(biāo)簽進(jìn)行數(shù)據(jù)上報(bào),創(chuàng)建一個(gè)臨時(shí)的img標(biāo)簽,將需要上報(bào)的數(shù)據(jù)附加在img的url后綴,從而上送到服務(wù)器。如例子所示:
var report = function(dataSrc){
    var img = new Image();  //創(chuàng)建image對(duì)象
    img.src = dataSrc;  //將要上送的數(shù)據(jù)url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
  • 可經(jīng)過(guò)排查發(fā)現(xiàn),使用report()函數(shù)存在30%丟失數(shù)據(jù)的情況。這是因?yàn)椋?code>img是report()函數(shù)中的局部變量,函數(shù)執(zhí)行完畢后就被銷(xiāo)毀了,而這個(gè)時(shí)候往往HTTP請(qǐng)求還沒(méi)建立成功。而通過(guò)閉包來(lái)保存img變量可以解決請(qǐng)求丟失的問(wèn)題:
//注意:我們將普通函數(shù)改成了自執(zhí)行函數(shù)
var report = (function(){
    var imgs = [];
    return function(dataSrc){
        var img = new Image();
        images.push(img);
        img.src = dataSrc;
    }
})();
  1. 用閉包實(shí)現(xiàn)面向?qū)ο?/strong>:我們經(jīng)常使用過(guò)程數(shù)據(jù)來(lái)描述面向?qū)ο缶幊坍?dāng)中的對(duì)象。對(duì)象的方法包含了過(guò)程,而閉包則是在過(guò)程中以執(zhí)行環(huán)境的方式包含了數(shù)據(jù)。
  • 既然閉包可以封裝私有變量,自然也能完成面向?qū)ο蟮脑O(shè)計(jì)。實(shí)際上,用面向?qū)ο笏枷肽軐?shí)現(xiàn)的功能,用閉包也能實(shí)現(xiàn),反之亦然,這就是JavaScript的靈活之處。
  • 有這樣一段面向?qū)ο蟮腏S代碼:
//Person構(gòu)造器,里面有一個(gè)name屬性
var Person = function(){
  this.name = "William";
};
//給Person的原型添加一個(gè)sayName()方法
Person.prototype.sayName = function(){
    console.info("hello,my name is " + this.name);
};
//實(shí)例化Person
var person1 = new Person();
person1.sayName();
  • 用閉包可以實(shí)現(xiàn)同樣的效果:因?yàn)樵贘avaScript用new執(zhí)行構(gòu)造函數(shù),本質(zhì)也是返回一個(gè)對(duì)象
//person()函數(shù)返回一個(gè)有sayName()方法的對(duì)象
var person = function(){
    var name = "William";
    return {
        sayName : function(){
            console.info("hello,my name is " + name);
        }
    }
};
//執(zhí)行person()函數(shù),將返回的對(duì)象賦值給person1
var person1 = person();
//調(diào)用person1.sayName()方法
person1.sayName();
//控制臺(tái)輸出 "hello,my name is William"
  1. 用閉包實(shí)現(xiàn)命令模式
  • 命令模式是將請(qǐng)求封裝成對(duì)象,從而可以把不同的請(qǐng)求對(duì)象進(jìn)行參數(shù)化、對(duì)請(qǐng)求對(duì)象排隊(duì)或者記錄日志以及執(zhí)行可撤銷(xiāo)的操作。
  • 命令模式的能夠分離請(qǐng)求發(fā)起者和執(zhí)行者之間的耦合關(guān)系。往往在命令被執(zhí)行之前,就預(yù)先往命令對(duì)象中植入命令的執(zhí)行者。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <button id="execute">開(kāi)啟</button>
        <button id="undo">關(guān)閉</button>
        <script type="text/javascript">
            var Tv = {
                open : function(){
                    console.info("打開(kāi)電視機(jī)");
                },
                close : function(){
                    console.info("關(guān)閉電視機(jī)");
                }
            };
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open();
            };
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close();
            };
            var setCommand = function(command){
                document.getElementById("execute").onclick = function(){
                    command.execute();
                }
                document.getElementById("undo").onclick = function(){
                    command.undo();
                }
            };
            //調(diào)用
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
  • 用閉包實(shí)現(xiàn)命令模式:
<script type="text/javascript">
    var Tv = {
        open : function(){
            console.info("打開(kāi)電視機(jī)");
        },
        close : function(){
            console.info("關(guān)閉電視機(jī)");
        }
    };
    var createCommand = function(receiver){
        var execute = function(){
            return receiver.open();
        }
        var undo = function(){
            return receiver.close();
        }
        return {
            execute : execute,
            undo : undo
        }
    };
    var setCommand = function(command){
        document.getElementById("execute").onclick = function(){
            command.execute();
        }
        document.getElementById("undo").onclick = function(){
            command.undo();
        }
    };
    //調(diào)用
    setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內(nèi)存管理
  • 一直流傳著一種聳人聽(tīng)聞的說(shuō)法,聲稱閉包會(huì)造成內(nèi)存泄漏,所以應(yīng)當(dāng)盡量避免使用閉包。
  • 局部變量本來(lái)應(yīng)該在函數(shù)退出的時(shí)候被釋放,但在閉包形成的環(huán)境中,局部變量不被釋放。從這個(gè)意義上看,確實(shí)會(huì)造成一些數(shù)據(jù)無(wú)法被及時(shí)銷(xiāo)毀。但我們使用閉包,是我們主動(dòng)選擇延長(zhǎng)局部變量的生命周期,不能說(shuō)成是內(nèi)存泄漏。當(dāng)使用完畢后,大可手動(dòng)將這些變量設(shè)為null
  • 而只有閉包形成循環(huán)引用的情況下,才會(huì)導(dǎo)致內(nèi)存泄漏。但這也不是閉包或者JavaScript的問(wèn)題,我們可以避免循環(huán)引用的情況,而不是因噎廢食,徹底摒棄閉包。

3.2 高階函數(shù)

  • 高階函數(shù)是指滿足以下兩個(gè)條件之一的函數(shù):
  1. 函數(shù)可以作為參數(shù)被傳遞;
  2. 函數(shù)可以作為返回值輸出;
  • 顯然,JavaScript語(yǔ)言中的函數(shù)兩個(gè)條件都滿足,下面將講解JavaScript高階函數(shù)特性的應(yīng)用示例。。
3.2.1 函數(shù)作為參數(shù)傳入
  • 把函數(shù)當(dāng)做參數(shù)傳遞,使得我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯。
  • 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定,或者jQuery中的ajax請(qǐng)求:
//按鈕監(jiān)聽(tīng)事件
$("btn").click(function(){
  console.info("btn clicked");
});
//可以發(fā)現(xiàn),其本質(zhì)就是執(zhí)行了click()方法,然后傳入一個(gè)函數(shù)作為參數(shù)。
//注意到:在按鈕點(diǎn)擊后的處理是變化的,通過(guò)回調(diào)函數(shù)來(lái)封裝變化。
  • 另外還有Array.sort()。這是用來(lái)排序數(shù)組的一個(gè)方法,傳入一個(gè)自定義的函數(shù)來(lái)指定是遞增還是遞減排序。
var arr = [1,7,9,2];
//從小到大排序
arr.sort(function(){
  return a - b;
});
console.info(arr); //輸出 "[1, 2, 7, 9]"
//從大道小排序
arr.sort(function(){
  return b - a;
});
console.info(arr); //輸出 "[9, 7, 2, 1]"
3.2.2 函數(shù)作為返回值輸出
  • 讓函數(shù)返回一個(gè)可執(zhí)行的函數(shù),在之前的代碼我們已經(jīng)接觸過(guò)了,這使得整個(gè)運(yùn)算過(guò)程是可延續(xù)。
  • 我們通過(guò)優(yōu)化一段類型判斷的JavaScript代碼來(lái)感受函數(shù)作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
    //通過(guò)傳入的obj對(duì)象執(zhí)行toString()方法,將結(jié)果值和預(yù)期字符串比較
    return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數(shù)組
var isArray = function(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數(shù)字
var isNumber = function(obj){
    return Object.prototype.toString.call(obj) === '[object Number]';
}
  • 可以發(fā)現(xiàn)上面的代碼toString部分都是相同的,我們通過(guò)將函數(shù)作為返回值的方式優(yōu)化代碼。
//抽象出一個(gè)類型判斷的通用函數(shù)
var isType = funcion(type){
    //該函數(shù)返回一個(gè)可執(zhí)行的函數(shù),用來(lái)執(zhí)行toString方法和預(yù)期字符串做比較
    return function(obj){
        return Object.prototype.toString.call(obj) === '[Object '+type+']';
    }
}
//預(yù)先注冊(cè)具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調(diào)用
console.info(isArray([1,3,2]));  //輸出: true
  • 另外一個(gè)例子,是利用JavaScript函數(shù)作為返回值這個(gè)特性實(shí)現(xiàn)單例模式。
var getSingle = function(fn){
    var ret;  //臨時(shí)變量
    return function(){
        //如果ret已經(jīng)存在的話則返回;否則新創(chuàng)建對(duì)象
        return ret || (ret = fn.apply(this,arguments));
    }
}
var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
console.info(script1 === script2);//輸出: true
3.2.3 高階函數(shù)實(shí)現(xiàn)AOP
  • AOP(面向切面編程)是指將日志統(tǒng)計(jì)、安全控制、異常處理等與業(yè)務(wù)邏輯無(wú)關(guān)的模塊代碼獨(dú)立出來(lái),通過(guò)“動(dòng)態(tài)植入”的方式參入到業(yè)務(wù)邏輯模塊當(dāng)中。這樣可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性。
  • Java語(yǔ)言可以通過(guò)反射和動(dòng)態(tài)代理機(jī)制來(lái)實(shí)現(xiàn)AOP技術(shù),而JavaScript函數(shù)作為返回值的特性就可以簡(jiǎn)單的實(shí)現(xiàn),這是JavaScript與生俱來(lái)的能力。
Function.prototype.invokeBefore = function(beforFn){
    var _self = this;   //原函數(shù)的引用
    return function(){
        //先執(zhí)行傳入的before函數(shù)
        beforFn.apply(this,arguments);
        //然后再執(zhí)行自身
        return _self.apply(this,arguments);
    }
}
Function.prototype.invokeAfter = function(afterFn){
    var _self = this;   //
    return function(){
        //先執(zhí)行函數(shù),并保存執(zhí)行結(jié)果
        var ret = _self.apply(this,arguments);
        //然后再執(zhí)行after函數(shù)
        afterFn.apply(this,arguments);
        //最后返回結(jié)果
        return ret;
    }
}
//定義一個(gè)方法,控制臺(tái)輸出2
var func = function(){
    console.info(2);
};
//指定func()函數(shù)執(zhí)行前和執(zhí)行后要做的事情
func = func.invokeBefore(function(){
    console.info(1);
}).invokeAfter(function(){
    console.info(3);
});
//調(diào)用func()函數(shù),控制臺(tái)輸出 1 2 3
func();
3.2.4 高階函數(shù)實(shí)現(xiàn)柯里化
  • 函數(shù)柯里化(function currying)的概念是由注明數(shù)理邏輯學(xué)家Haskell Curry豐富和發(fā)展起來(lái)的,所以因此得名。
  • currying又稱為部分求值。currying函數(shù)首先接受一些參數(shù),接受這些參數(shù)之后并不立即求值,而是返回另外一個(gè)函數(shù),并將傳入的參數(shù)函數(shù)保存起來(lái).等真正需要求值的時(shí)候,將之前傳入的所有參數(shù)一次性的求值.
  • 我們通過(guò)JavaScript,通過(guò)一個(gè)記賬的代碼來(lái)模擬currying函數(shù)
var currying = function(fn){
    var args = [];  //緩存對(duì)象
    return function(){
        if(arguments.length == 0){
            //如果傳入的參數(shù)為空,則直接返回結(jié)果
            return fn.apply(this,args);
        }else{
            //如果參數(shù)不為空,則將傳入?yún)?shù)push到args數(shù)組中緩存起來(lái)
            [].push.apply(args,arguments);
            //并返回函數(shù)本身
            return arguments.callee;
        }
    }
}
var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0;l = arguments.length;i<l;i++){
            money += arguments[i];
        }
        return money;
    }
});
//轉(zhuǎn)換成currying函數(shù)
var cost = currying(cost);
cost(100);  //記賬100,未真正求值
cost(100);  //記賬100,未真正求值
cost(400);  //記賬400,未真正求值
console.info(cost());   //求值,并輸出:600
3.2.5 高階函數(shù)實(shí)現(xiàn)反柯里化
  • 通過(guò)call()apply()方法可以借用別的對(duì)象的方法,比如借用Array.prototype.push()方法.那么有沒(méi)有辦法將借用的方法提取出來(lái)呢?uncurrying就是用來(lái)解決這個(gè)問(wèn)題的.
//為Function對(duì)象的原型添加uncurrying方法
Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        var obj = Array.prototype.call(arguments);
        return self.apply(obj,arguments);
    }
}
//提取push方法并使用
var push = Array.prototype.uncurrying();
(function(){
    push(arguments,4);
    console.info(arguments);//輸出 [1,2,3,4]
})(1,2,3);
3.2.6 高階函數(shù)實(shí)現(xiàn)函數(shù)節(jié)流
  • JavaScript中的函數(shù)大多數(shù)都是由用戶主動(dòng)觸發(fā)的,尤其在瀏覽器端的某些情況下函數(shù)被非常頻繁的調(diào)用,從而導(dǎo)致性能問(wèn)題。
  • 比如用來(lái)監(jiān)聽(tīng)瀏覽器窗口大小的window.onresize事件,當(dāng)瀏覽器窗口被不斷拉伸時(shí),這個(gè)事件觸發(fā)的頻率會(huì)非常高;又比如元素的拖拽監(jiān)聽(tīng)事件onmousemove,如果元素被不停的拖拽,也會(huì)頻繁的觸發(fā);還有最典型的監(jiān)聽(tīng)文件上傳進(jìn)度的事件,由于需要不斷掃描文件用以在頁(yè)面中顯示掃描進(jìn)度。導(dǎo)致通知的頻率非常之高,大約一秒鐘10次,遠(yuǎn)超過(guò)人眼所能覺(jué)察的極限。
  • throttle函數(shù)就是解決此類問(wèn)題的方案。throttle顧名思義節(jié)流器,借鑒的是工程學(xué)里的思想,比如用節(jié)流器來(lái)穩(wěn)定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過(guò)忽略短時(shí)間內(nèi)函數(shù)的密集執(zhí)行,達(dá)到穩(wěn)定性能的作用。
var throttle = function(fn,interval){
    var _self = fn,
            timer,
            firstTime = true;
    return function(){
        var args = arguments,
                _me = this;
        if(firstTime){
            _self.apply(_me,args);
            return firstTime = false;
        }
        if(timer){
            return false;
        }
        timer = setTimeout(function(){
            clearTimeout(timer);
            timer = null;
            _self.apply(_me,args);
        },interval || 500);
    };
};
window.onresize = throttle(function(){
    console.info("resize come in");
},500);
3.2.7 高階函數(shù)實(shí)現(xiàn)分時(shí)函數(shù)
  • 函數(shù)節(jié)流是限制函數(shù)被頻繁調(diào)用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時(shí)也影響著頁(yè)面的性能。比如WebQQ加載好友列表,往往需要短時(shí)間內(nèi)一次性創(chuàng)建成百上千個(gè)節(jié)點(diǎn),嚴(yán)重影響頁(yè)面性能。
//模擬添加1000個(gè)數(shù)據(jù)
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = function(data){
    for (var i=0;l=data.length;i<l;i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
};
renderFriendList(ary);
  • 通過(guò)分時(shí)函數(shù)讓創(chuàng)建節(jié)點(diǎn)的工作分批進(jìn)行。
//創(chuàng)建timeChunk函數(shù)
var timeChunk = function(ary,fn,count){
    var obj,t,len = ary.length;
    var start = function(){
        for (var i=0;i<Math.min(count || 1,ary.length);i++) {
            var obj = ary.shift();
            fn(obj);
        }
    }
    return function(){
        t = setInterval(function(){
            if(ary.length === 0){
                return clearInterval(t);
            }
            start();
        },200);
    }
};
//測(cè)試
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = timeChunk(ary,function(n){
    var div = document.createElement('div');
    div.innerHTML = i;
    document.body.appendChild(div);
},8);
renderFriendList(ary);
  • 除此之外,書(shū)中還有通過(guò)高階函數(shù)的特性實(shí)現(xiàn)惰性加載函數(shù)的案例,考慮到文章篇幅的關(guān)系,這里就不贅述了。

3.1 閉包

3.1.1 變量的作用域

  • 所謂變量的作用域,就是變量的有效范圍。通過(guò)作用域的劃分,JavaScript變量分為全局變量和局部變量。
  • 聲明在函數(shù)外的變量為全局變量;在函數(shù)內(nèi)并且以var關(guān)鍵字聲明的變量為局部變量
  • 我們都知道,全局變量能在任何作用域訪問(wèn)到,但這很容易造成命名沖突;而局部變量只有在函數(shù)里面能訪問(wèn)到,這是因?yàn)镴avaScript的查找變量的規(guī)則是從內(nèi)往外搜索的。

3.1.2 變量的生命周期

  • 全局變量的生命周期是永久的(除非我們主動(dòng)銷(xiāo)毀這個(gè)全局變量),而局部變量則當(dāng)函數(shù)執(zhí)行完畢時(shí)被銷(xiāo)毀。
  • 那JavaScript中是否存在,即便函數(shù)執(zhí)行完畢,依然不會(huì)被銷(xiāo)毀的局部變量?答案是肯定的。
<script type="text/javascript">
    //現(xiàn)在有一個(gè)名為func的函數(shù)
    var func = function(){
        //①函數(shù)執(zhí)行體中,將局部變量a賦值為1
        var a = 1;
        //②返回一個(gè)function執(zhí)行環(huán)境
        return function(){
            //③執(zhí)行環(huán)境中,將func.a局部變量加1,然后輸出到控制臺(tái)
            a++;
            console.info(a);
        }
    };
    
    //調(diào)用:將func函數(shù)執(zhí)行后的返回,賦值給f
    var f = func();
    f();    //f()調(diào)用一次,輸出2
    f();    //f()再調(diào)用一次,輸出3
    f();    //f()接著調(diào)用,輸出4
</script>
  • 以上案例中的func.a局部變量,在func()函數(shù)執(zhí)行過(guò)后并沒(méi)有被銷(xiāo)毀。每次執(zhí)行f()時(shí),仍能對(duì)它進(jìn)行累加,就是佐證。這是因?yàn)?code>func()返回了一個(gè)匿名函數(shù)的引用賦值給f,正是由于被外部變量引用了,所以不被銷(xiāo)毀,此時(shí)這個(gè)匿名函數(shù)就稱為閉包
  • 什么是閉包?

    在一個(gè)函數(shù)內(nèi)定義另外一個(gè)函數(shù)(內(nèi)部函數(shù)可以訪問(wèn)外部函數(shù)的變量),如果將這個(gè)內(nèi)部函數(shù)提供給其他變量引用時(shí),內(nèi)部函數(shù)作用域以及依賴的外部作用域的執(zhí)行環(huán)境就不會(huì)被銷(xiāo)毀。此時(shí)這個(gè)內(nèi)部函數(shù)就像一個(gè)可以訪問(wèn)封閉數(shù)據(jù)包的執(zhí)行環(huán)境,也就是閉包。

3.1.3 閉包的用途

  • 我們不但要學(xué)習(xí)什么是JavaScript閉包,更要了解如何利用閉包特性來(lái)寫(xiě)代碼。由于篇幅有限,書(shū)中只羅列了幾個(gè)使用閉包的例子,但要知道實(shí)際開(kāi)發(fā)中運(yùn)用閉包非常廣泛,遠(yuǎn)不止于此。
  1. 封裝變量:通過(guò)閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
    var name = "William";
    return function(){
        console.info(name);          
    };
})();
person();   // 輸出成功
console.info(person.name);  //// 輸出失敗
  1. 延續(xù)變量的生命周期:我們經(jīng)常用<img>標(biāo)簽進(jìn)行數(shù)據(jù)上報(bào),創(chuàng)建一個(gè)臨時(shí)的img標(biāo)簽,將需要上報(bào)的數(shù)據(jù)附加在img的url后綴,從而上送到服務(wù)器。如例子所示:
var report = function(dataSrc){
    var img = new Image();  //創(chuàng)建image對(duì)象
    img.src = dataSrc;  //將要上送的數(shù)據(jù)url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
  • 可經(jīng)過(guò)排查發(fā)現(xiàn),使用report()函數(shù)存在30%丟失數(shù)據(jù)的情況。這是因?yàn)椋?code>img是report()函數(shù)中的局部變量,函數(shù)執(zhí)行完畢后就被銷(xiāo)毀了,而這個(gè)時(shí)候往往HTTP請(qǐng)求還沒(méi)建立成功。而通過(guò)閉包來(lái)保存img變量可以解決請(qǐng)求丟失的問(wèn)題:
//注意:我們將普通函數(shù)改成了自執(zhí)行函數(shù)
var report = (function(){
    var imgs = [];
    return function(dataSrc){
        var img = new Image();
        images.push(img);
        img.src = dataSrc;
    }
})();
  1. 用閉包實(shí)現(xiàn)面向?qū)ο?/strong>:我們經(jīng)常使用過(guò)程數(shù)據(jù)來(lái)描述面向?qū)ο缶幊坍?dāng)中的對(duì)象。對(duì)象的方法包含了過(guò)程,而閉包則是在過(guò)程中以執(zhí)行環(huán)境的方式包含了數(shù)據(jù)。
  • 既然閉包可以封裝私有變量,自然也能完成面向?qū)ο蟮脑O(shè)計(jì)。實(shí)際上,用面向?qū)ο笏枷肽軐?shí)現(xiàn)的功能,用閉包也能實(shí)現(xiàn),反之亦然,這就是JavaScript的靈活之處。
  • 有這樣一段面向?qū)ο蟮腏S代碼:
//Person構(gòu)造器,里面有一個(gè)name屬性
var Person = function(){
  this.name = "William";
};
//給Person的原型添加一個(gè)sayName()方法
Person.prototype.sayName = function(){
    console.info("hello,my name is " + this.name);
};
//實(shí)例化Person
var person1 = new Person();
person1.sayName();
  • 用閉包可以實(shí)現(xiàn)同樣的效果:因?yàn)樵贘avaScript用new執(zhí)行構(gòu)造函數(shù),本質(zhì)也是返回一個(gè)對(duì)象
//person()函數(shù)返回一個(gè)有sayName()方法的對(duì)象
var person = function(){
    var name = "William";
    return {
        sayName : function(){
            console.info("hello,my name is " + name);
        }
    }
};
//執(zhí)行person()函數(shù),將返回的對(duì)象賦值給person1
var person1 = person();
//調(diào)用person1.sayName()方法
person1.sayName();
//控制臺(tái)輸出 "hello,my name is William"
  1. 用閉包實(shí)現(xiàn)命令模式
  • 命令模式是將請(qǐng)求封裝成對(duì)象,從而可以把不同的請(qǐng)求對(duì)象進(jìn)行參數(shù)化、對(duì)請(qǐng)求對(duì)象排隊(duì)或者記錄日志以及執(zhí)行可撤銷(xiāo)的操作。
  • 命令模式的能夠分離請(qǐng)求發(fā)起者和執(zhí)行者之間的耦合關(guān)系。往往在命令被執(zhí)行之前,就預(yù)先往命令對(duì)象中植入命令的執(zhí)行者。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <button id="execute">開(kāi)啟</button>
        <button id="undo">關(guān)閉</button>
        <script type="text/javascript">
            var Tv = {
                open : function(){
                    console.info("打開(kāi)電視機(jī)");
                },
                close : function(){
                    console.info("關(guān)閉電視機(jī)");
                }
            };
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open();
            };
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close();
            };
            var setCommand = function(command){
                document.getElementById("execute").onclick = function(){
                    command.execute();
                }
                document.getElementById("undo").onclick = function(){
                    command.undo();
                }
            };
            //調(diào)用
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
  • 用閉包實(shí)現(xiàn)命令模式:
<script type="text/javascript">
    var Tv = {
        open : function(){
            console.info("打開(kāi)電視機(jī)");
        },
        close : function(){
            console.info("關(guān)閉電視機(jī)");
        }
    };
    var createCommand = function(receiver){
        var execute = function(){
            return receiver.open();
        }
        var undo = function(){
            return receiver.close();
        }
        return {
            execute : execute,
            undo : undo
        }
    };
    var setCommand = function(command){
        document.getElementById("execute").onclick = function(){
            command.execute();
        }
        document.getElementById("undo").onclick = function(){
            command.undo();
        }
    };
    //調(diào)用
    setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內(nèi)存管理
  • 一直流傳著一種聳人聽(tīng)聞的說(shuō)法,聲稱閉包會(huì)造成內(nèi)存泄漏,所以應(yīng)當(dāng)盡量避免使用閉包。
  • 局部變量本來(lái)應(yīng)該在函數(shù)退出的時(shí)候被釋放,但在閉包形成的環(huán)境中,局部變量不被釋放。從這個(gè)意義上看,確實(shí)會(huì)造成一些數(shù)據(jù)無(wú)法被及時(shí)銷(xiāo)毀。但我們使用閉包,是我們主動(dòng)選擇延長(zhǎng)局部變量的生命周期,不能說(shuō)成是內(nèi)存泄漏。當(dāng)使用完畢后,大可手動(dòng)將這些變量設(shè)為null
  • 而只有閉包形成循環(huán)引用的情況下,才會(huì)導(dǎo)致內(nèi)存泄漏。但這也不是閉包或者JavaScript的問(wèn)題,我們可以避免循環(huán)引用的情況,而不是因噎廢食,徹底摒棄閉包。

3.2 高階函數(shù)

  • 高階函數(shù)是指滿足以下兩個(gè)條件之一的函數(shù):
  1. 函數(shù)可以作為參數(shù)被傳遞;
  2. 函數(shù)可以作為返回值輸出;
  • 顯然,JavaScript語(yǔ)言中的函數(shù)兩個(gè)條件都滿足,下面將講解JavaScript高階函數(shù)特性的應(yīng)用示例。。
3.2.1 函數(shù)作為參數(shù)傳入
  • 把函數(shù)當(dāng)做參數(shù)傳遞,使得我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯。
  • 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定,或者jQuery中的ajax請(qǐng)求:
//按鈕監(jiān)聽(tīng)事件
$("btn").click(function(){
  console.info("btn clicked");
});
//可以發(fā)現(xiàn),其本質(zhì)就是執(zhí)行了click()方法,然后傳入一個(gè)函數(shù)作為參數(shù)。
//注意到:在按鈕點(diǎn)擊后的處理是變化的,通過(guò)回調(diào)函數(shù)來(lái)封裝變化。
  • 另外還有Array.sort()。這是用來(lái)排序數(shù)組的一個(gè)方法,傳入一個(gè)自定義的函數(shù)來(lái)指定是遞增還是遞減排序。
var arr = [1,7,9,2];
//從小到大排序
arr.sort(function(){
  return a - b;
});
console.info(arr); //輸出 "[1, 2, 7, 9]"
//從大道小排序
arr.sort(function(){
  return b - a;
});
console.info(arr); //輸出 "[9, 7, 2, 1]"
3.2.2 函數(shù)作為返回值輸出
  • 讓函數(shù)返回一個(gè)可執(zhí)行的函數(shù),在之前的代碼我們已經(jīng)接觸過(guò)了,這使得整個(gè)運(yùn)算過(guò)程是可延續(xù)。
  • 我們通過(guò)優(yōu)化一段類型判斷的JavaScript代碼來(lái)感受函數(shù)作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
    //通過(guò)傳入的obj對(duì)象執(zhí)行toString()方法,將結(jié)果值和預(yù)期字符串比較
    return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數(shù)組
var isArray = function(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數(shù)字
var isNumber = function(obj){
    return Object.prototype.toString.call(obj) === '[object Number]';
}
  • 可以發(fā)現(xiàn)上面的代碼toString部分都是相同的,我們通過(guò)將函數(shù)作為返回值的方式優(yōu)化代碼。
//抽象出一個(gè)類型判斷的通用函數(shù)
var isType = funcion(type){
    //該函數(shù)返回一個(gè)可執(zhí)行的函數(shù),用來(lái)執(zhí)行toString方法和預(yù)期字符串做比較
    return function(obj){
        return Object.prototype.toString.call(obj) === '[Object '+type+']';
    }
}
//預(yù)先注冊(cè)具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調(diào)用
console.info(isArray([1,3,2]));  //輸出: true
  • 另外一個(gè)例子,是利用JavaScript函數(shù)作為返回值這個(gè)特性實(shí)現(xiàn)單例模式。
var getSingle = function(fn){
    var ret;  //臨時(shí)變量
    return function(){
        //如果ret已經(jīng)存在的話則返回;否則新創(chuàng)建對(duì)象
        return ret || (ret = fn.apply(this,arguments));
    }
}
var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
console.info(script1 === script2);//輸出: true
3.2.3 高階函數(shù)實(shí)現(xiàn)AOP
  • AOP(面向切面編程)是指將日志統(tǒng)計(jì)、安全控制、異常處理等與業(yè)務(wù)邏輯無(wú)關(guān)的模塊代碼獨(dú)立出來(lái),通過(guò)“動(dòng)態(tài)植入”的方式參入到業(yè)務(wù)邏輯模塊當(dāng)中。這樣可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性。
  • Java語(yǔ)言可以通過(guò)反射和動(dòng)態(tài)代理機(jī)制來(lái)實(shí)現(xiàn)AOP技術(shù),而JavaScript函數(shù)作為返回值的特性就可以簡(jiǎn)單的實(shí)現(xiàn),這是JavaScript與生俱來(lái)的能力。
Function.prototype.invokeBefore = function(beforFn){
    var _self = this;   //原函數(shù)的引用
    return function(){
        //先執(zhí)行傳入的before函數(shù)
        beforFn.apply(this,arguments);
        //然后再執(zhí)行自身
        return _self.apply(this,arguments);
    }
}
Function.prototype.invokeAfter = function(afterFn){
    var _self = this;   //
    return function(){
        //先執(zhí)行函數(shù),并保存執(zhí)行結(jié)果
        var ret = _self.apply(this,arguments);
        //然后再執(zhí)行after函數(shù)
        afterFn.apply(this,arguments);
        //最后返回結(jié)果
        return ret;
    }
}
//定義一個(gè)方法,控制臺(tái)輸出2
var func = function(){
    console.info(2);
};
//指定func()函數(shù)執(zhí)行前和執(zhí)行后要做的事情
func = func.invokeBefore(function(){
    console.info(1);
}).invokeAfter(function(){
    console.info(3);
});
//調(diào)用func()函數(shù),控制臺(tái)輸出 1 2 3
func();
3.2.4 高階函數(shù)實(shí)現(xiàn)柯里化
  • 函數(shù)柯里化(function currying)的概念是由注明數(shù)理邏輯學(xué)家Haskell Curry豐富和發(fā)展起來(lái)的,所以因此得名。
  • currying又稱為部分求值。currying函數(shù)首先接受一些參數(shù),接受這些參數(shù)之后并不立即求值,而是返回另外一個(gè)函數(shù),并將傳入的參數(shù)函數(shù)保存起來(lái).等真正需要求值的時(shí)候,將之前傳入的所有參數(shù)一次性的求值.
  • 我們通過(guò)JavaScript,通過(guò)一個(gè)記賬的代碼來(lái)模擬currying函數(shù)
var currying = function(fn){
    var args = [];  //緩存對(duì)象
    return function(){
        if(arguments.length == 0){
            //如果傳入的參數(shù)為空,則直接返回結(jié)果
            return fn.apply(this,args);
        }else{
            //如果參數(shù)不為空,則將傳入?yún)?shù)push到args數(shù)組中緩存起來(lái)
            [].push.apply(args,arguments);
            //并返回函數(shù)本身
            return arguments.callee;
        }
    }
}
var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0;l = arguments.length;i<l;i++){
            money += arguments[i];
        }
        return money;
    }
});
//轉(zhuǎn)換成currying函數(shù)
var cost = currying(cost);
cost(100);  //記賬100,未真正求值
cost(100);  //記賬100,未真正求值
cost(400);  //記賬400,未真正求值
console.info(cost());   //求值,并輸出:600
3.2.5 高階函數(shù)實(shí)現(xiàn)反柯里化
  • 通過(guò)call()apply()方法可以借用別的對(duì)象的方法,比如借用Array.prototype.push()方法.那么有沒(méi)有辦法將借用的方法提取出來(lái)呢?uncurrying就是用來(lái)解決這個(gè)問(wèn)題的.
//為Function對(duì)象的原型添加uncurrying方法
Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        var obj = Array.prototype.call(arguments);
        return self.apply(obj,arguments);
    }
}
//提取push方法并使用
var push = Array.prototype.uncurrying();
(function(){
    push(arguments,4);
    console.info(arguments);//輸出 [1,2,3,4]
})(1,2,3);
3.2.6 高階函數(shù)實(shí)現(xiàn)函數(shù)節(jié)流
  • JavaScript中的函數(shù)大多數(shù)都是由用戶主動(dòng)觸發(fā)的,尤其在瀏覽器端的某些情況下函數(shù)被非常頻繁的調(diào)用,從而導(dǎo)致性能問(wèn)題。
  • 比如用來(lái)監(jiān)聽(tīng)瀏覽器窗口大小的window.onresize事件,當(dāng)瀏覽器窗口被不斷拉伸時(shí),這個(gè)事件觸發(fā)的頻率會(huì)非常高;又比如元素的拖拽監(jiān)聽(tīng)事件onmousemove,如果元素被不停的拖拽,也會(huì)頻繁的觸發(fā);還有最典型的監(jiān)聽(tīng)文件上傳進(jìn)度的事件,由于需要不斷掃描文件用以在頁(yè)面中顯示掃描進(jìn)度。導(dǎo)致通知的頻率非常之高,大約一秒鐘10次,遠(yuǎn)超過(guò)人眼所能覺(jué)察的極限。
  • throttle函數(shù)就是解決此類問(wèn)題的方案。throttle顧名思義節(jié)流器,借鑒的是工程學(xué)里的思想,比如用節(jié)流器來(lái)穩(wěn)定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過(guò)忽略短時(shí)間內(nèi)函數(shù)的密集執(zhí)行,達(dá)到穩(wěn)定性能的作用。
var throttle = function(fn,interval){
    var _self = fn,
            timer,
            firstTime = true;
    return function(){
        var args = arguments,
                _me = this;
        if(firstTime){
            _self.apply(_me,args);
            return firstTime = false;
        }
        if(timer){
            return false;
        }
        timer = setTimeout(function(){
            clearTimeout(timer);
            timer = null;
            _self.apply(_me,args);
        },interval || 500);
    };
};
window.onresize = throttle(function(){
    console.info("resize come in");
},500);
3.2.7 高階函數(shù)實(shí)現(xiàn)分時(shí)函數(shù)
  • 函數(shù)節(jié)流是限制函數(shù)被頻繁調(diào)用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時(shí)也影響著頁(yè)面的性能。比如WebQQ加載好友列表,往往需要短時(shí)間內(nèi)一次性創(chuàng)建成百上千個(gè)節(jié)點(diǎn),嚴(yán)重影響頁(yè)面性能。
//模擬添加1000個(gè)數(shù)據(jù)
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = function(data){
    for (var i=0;l=data.length;i<l;i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
};
renderFriendList(ary);
  • 通過(guò)分時(shí)函數(shù)讓創(chuàng)建節(jié)點(diǎn)的工作分批進(jìn)行。
//創(chuàng)建timeChunk函數(shù)
var timeChunk = function(ary,fn,count){
    var obj,t,len = ary.length;
    var start = function(){
        for (var i=0;i<Math.min(count || 1,ary.length);i++) {
            var obj = ary.shift();
            fn(obj);
        }
    }
    return function(){
        t = setInterval(function(){
            if(ary.length === 0){
                return clearInterval(t);
            }
            start();
        },200);
    }
};
//測(cè)試
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = timeChunk(ary,function(n){
    var div = document.createElement('div');
    div.innerHTML = i;
    document.body.appendChild(div);
},8);
renderFriendList(ary);
  • 除此之外,書(shū)中還有通過(guò)高階函數(shù)的特性實(shí)現(xiàn)惰性加載函數(shù)的案例,考慮到文章篇幅的關(guān)系,這里就不贅述了。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容