三景东、閉包和高階函數(shù)
IMG_20170112_004128.jpg
3.1 閉包
3.1.1 變量的作用域
- 所謂變量的作用域闻察,就是變量的有效范圍邻梆。通過作用域的劃分守伸,JavaScript變量分為全局變量和局部變量。
- 聲明在函數(shù)外的變量為
全局變量
浦妄;在函數(shù)內(nèi)并且以var
關(guān)鍵字聲明的變量為局部變量
尼摹。 - 我們都知道,全局變量能在任何作用域訪問到剂娄,但這很容易造成命名沖突蠢涝;而局部變量只有在函數(shù)里面能訪問到,這是因?yàn)镴avaScript的查找變量的規(guī)則是從內(nèi)往外搜索的阅懦。
3.1.2 變量的生命周期
- 全局變量的生命周期是永久的(除非我們主動(dòng)銷毀這個(gè)全局變量)和二,而局部變量則當(dāng)函數(shù)執(zhí)行完畢時(shí)被銷毀。
- 那JavaScript中是否存在耳胎,即便函數(shù)執(zhí)行完畢惯吕,依然不會(huì)被銷毀的局部變量惕它?答案是肯定的。
<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í)行過后并沒有被銷毀。每次執(zhí)行f()
時(shí)半醉,仍能對(duì)它進(jìn)行累加疚俱,就是佐證。這是因?yàn)?code>func()返回了一個(gè)匿名函數(shù)的引用賦值給f
缩多,正是由于被外部變量引用了呆奕,所以不被銷毀,此時(shí)這個(gè)匿名函數(shù)就稱為閉包
衬吆。 - 什么是閉包梁钾?
在一個(gè)函數(shù)內(nèi)定義另外一個(gè)函數(shù)(內(nèi)部函數(shù)可以訪問外部函數(shù)的變量),如果將這個(gè)內(nèi)部函數(shù)提供給其他變量引用時(shí)逊抡,內(nèi)部函數(shù)作用域以及依賴的外部作用域的執(zhí)行環(huán)境就不會(huì)被銷毀姆泻。此時(shí)這個(gè)內(nèi)部函數(shù)就像一個(gè)可以訪問封閉數(shù)據(jù)包的執(zhí)行環(huán)境冒嫡,也就是閉包。
3.1.3 閉包的用途
- 我們不但要學(xué)習(xí)什么是JavaScript閉包方咆,更要了解如何利用閉包特性來寫代碼。由于篇幅有限蟀架,書中只羅列了幾個(gè)使用閉包的例子辜窑,但要知道實(shí)際開發(fā)中運(yùn)用閉包非常廣泛钩述,遠(yuǎn)不止于此。
- 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
var name = "William";
return function(){
console.info(name);
};
})();
person(); // 輸出成功
console.info(person.name); //// 輸出失敗
-
延續(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)過排查發(fā)現(xiàn)颓屑,使用
report()
函數(shù)存在30%丟失數(shù)據(jù)的情況揪惦。這是因?yàn)椋?code>img是report()
函數(shù)中的局部變量罗侯,函數(shù)執(zhí)行完畢后就被銷毀了钩杰,而這個(gè)時(shí)候往往HTTP請(qǐng)求還沒建立成功讲弄。而通過閉包來保存img
變量可以解決請(qǐng)求丟失的問題:
//注意:我們將普通函數(shù)改成了自執(zhí)行函數(shù)
var report = (function(){
var imgs = [];
return function(dataSrc){
var img = new Image();
images.push(img);
img.src = dataSrc;
}
})();
-
用閉包實(shí)現(xiàn)面向?qū)ο?/strong>:我們經(jīng)常使用
過程
和數(shù)據(jù)
來描述面向?qū)ο缶幊坍?dāng)中的對(duì)象。對(duì)象的方法包含了過程避除,而閉包則是在過程中以執(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"
- 用閉包實(shí)現(xiàn)命令模式
- 命令模式是將請(qǐng)求封裝成對(duì)象被辑,從而可以把不同的請(qǐng)求對(duì)象進(jìn)行參數(shù)化盼理、對(duì)請(qǐng)求對(duì)象排隊(duì)或者記錄日志以及執(zhí)行可撤銷的操作宏怔。
- 命令模式的能夠分離請(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">開啟</button>
<button id="undo">關(guān)閉</button>
<script type="text/javascript">
var Tv = {
open : function(){
console.info("打開電視機(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("打開電視機(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)存管理
- 一直流傳著一種聳人聽聞的說法,聲稱閉包會(huì)造成內(nèi)存泄漏位他,所以應(yīng)當(dāng)盡量避免使用閉包棱诱。
- 局部變量本來應(yīng)該在函數(shù)退出的時(shí)候被釋放迈勋,但在閉包形成的環(huán)境中靡菇,局部變量不被釋放厦凤。從這個(gè)意義上看较鼓,確實(shí)會(huì)造成一些數(shù)據(jù)無法被及時(shí)銷毀博烂。但我們使用閉包禽篱,是我們主動(dòng)選擇延長(zhǎng)局部變量的生命周期,不能說成是內(nèi)存泄漏玛界。當(dāng)使用完畢后慎框,大可手動(dòng)將這些變量設(shè)為
null
鲤脏。 - 而只有閉包形成循環(huán)引用的情況下,才會(huì)導(dǎo)致內(nèi)存泄漏努溃。但這也不是閉包或者JavaScript的問題梧税,我們可以避免循環(huán)引用的情況第队,而不是因噎廢食凳谦,徹底摒棄閉包尸执。
3.2 高階函數(shù)
-
高階函數(shù)
是指滿足以下兩個(gè)條件之一的函數(shù):
- 函數(shù)可以作為參數(shù)被傳遞如失;
- 函數(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)聽事件
$("btn").click(function(){
console.info("btn clicked");
});
//可以發(fā)現(xiàn),其本質(zhì)就是執(zhí)行了click()方法泪掀,然后傳入一個(gè)函數(shù)作為參數(shù)。
//注意到:在按鈕點(diǎn)擊后的處理是變化的祝辣,通過回調(diào)函數(shù)來封裝變化蝙斜。
- 另外還有
Array.sort()
孕荠。這是用來排序數(shù)組的一個(gè)方法稚伍,傳入一個(gè)自定義的函數(shù)來指定是遞增還是遞減排序个曙。
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)接觸過了猴贰,這使得整個(gè)運(yùn)算過程是可延續(xù)米绕。
- 我們通過優(yōu)化一段類型判斷的JavaScript代碼來感受函數(shù)作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
//通過傳入的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部分都是相同的,我們通過將函數(shù)作為返回值的方式優(yōu)化代碼劫笙。
//抽象出一個(gè)類型判斷的通用函數(shù)
var isType = funcion(type){
//該函數(shù)返回一個(gè)可執(zhí)行的函數(shù)填大,用來執(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ù)邏輯無關(guān)的模塊代碼獨(dú)立出來梆造,通過“動(dòng)態(tài)植入”的方式參入到業(yè)務(wù)邏輯模塊當(dāng)中镇辉。這樣可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性摊聋。
- Java語(yǔ)言可以通過反射和動(dòng)態(tài)代理機(jī)制來實(shí)現(xiàn)AOP技術(shù)麻裁,而JavaScript函數(shù)作為返回值的特性就可以簡(jiǎn)單的實(shí)現(xiàn)煎源,這是JavaScript與生俱來的能力。
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ā)展起來的给猾,所以因此得名敢伸。
- currying又稱為
部分求值
详拙。currying函數(shù)首先接受一些參數(shù),接受這些參數(shù)之后并不立即求值,而是返回另外一個(gè)函數(shù),并將傳入的參數(shù)函數(shù)保存起來.等真正需要求值的時(shí)候,將之前傳入的所有參數(shù)一次性的求值. - 我們通過JavaScript,通過一個(gè)記賬的代碼來模擬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ù)組中緩存起來
[].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)反柯里化
- 通過
call()
和apply()
方法可以借用別的對(duì)象的方法,比如借用Array.prototype.push()
方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個(gè)問題的.
//為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)致性能問題。
- 比如用來監(jiān)聽瀏覽器窗口大小的
window.onresize
事件,當(dāng)瀏覽器窗口被不斷拉伸時(shí),這個(gè)事件觸發(fā)的頻率會(huì)非常高;又比如元素的拖拽監(jiān)聽事件onmousemove
,如果元素被不停的拖拽,也會(huì)頻繁的觸發(fā)汁掠;還有最典型的監(jiān)聽文件上傳進(jìn)度的事件晋南,由于需要不斷掃描文件用以在頁(yè)面中顯示掃描進(jìn)度。導(dǎo)致通知的頻率非常之高姜凄,大約一秒鐘10次态秧,遠(yuǎn)超過人眼所能覺察的極限。 - throttle函數(shù)就是解決此類問題的方案捐友。
throttle
顧名思義節(jié)流器,借鑒的是工程學(xué)里的思想,比如用節(jié)流器來穩(wěn)定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時(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);
- 通過分時(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ù)的特性實(shí)現(xiàn)惰性加載函數(shù)的案例瓮恭,考慮到文章篇幅的關(guān)系屯蹦,這里就不贅述了登澜。
3.1 閉包
3.1.1 變量的作用域
- 所謂變量的作用域,就是變量的有效范圍谴仙。通過作用域的劃分晃跺,JavaScript變量分為全局變量和局部變量哼审。
- 聲明在函數(shù)外的變量為
全局變量
;在函數(shù)內(nèi)并且以var
關(guān)鍵字聲明的變量為局部變量
春霍。 - 我們都知道址儒,全局變量能在任何作用域訪問到莲趣,但這很容易造成命名沖突;而局部變量只有在函數(shù)里面能訪問到潘鲫,這是因?yàn)镴avaScript的查找變量的規(guī)則是從內(nèi)往外搜索的溉仑。
3.1.2 變量的生命周期
- 全局變量的生命周期是永久的(除非我們主動(dòng)銷毀這個(gè)全局變量)浊竟,而局部變量則當(dāng)函數(shù)執(zhí)行完畢時(shí)被銷毀怨喘。
- 那JavaScript中是否存在,即便函數(shù)執(zhí)行完畢振定,依然不會(huì)被銷毀的局部變量哲思?答案是肯定的。
<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í)行過后并沒有被銷毀抱环。每次執(zhí)行f()
時(shí),仍能對(duì)它進(jìn)行累加纸巷,就是佐證镇草。這是因?yàn)?code>func()返回了一個(gè)匿名函數(shù)的引用賦值給f
,正是由于被外部變量引用了瘤旨,所以不被銷毀梯啤,此時(shí)這個(gè)匿名函數(shù)就稱為閉包
。 - 什么是閉包存哲?
在一個(gè)函數(shù)內(nèi)定義另外一個(gè)函數(shù)(內(nèi)部函數(shù)可以訪問外部函數(shù)的變量)因宇,如果將這個(gè)內(nèi)部函數(shù)提供給其他變量引用時(shí),內(nèi)部函數(shù)作用域以及依賴的外部作用域的執(zhí)行環(huán)境就不會(huì)被銷毀祟偷。此時(shí)這個(gè)內(nèi)部函數(shù)就像一個(gè)可以訪問封閉數(shù)據(jù)包的執(zhí)行環(huán)境察滑,也就是閉包。
3.1.3 閉包的用途
- 我們不但要學(xué)習(xí)什么是JavaScript閉包修肠,更要了解如何利用閉包特性來寫代碼贺辰。由于篇幅有限,書中只羅列了幾個(gè)使用閉包的例子,但要知道實(shí)際開發(fā)中運(yùn)用閉包非常廣泛魂爪,遠(yuǎn)不止于此先舷。
- 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
var name = "William";
return function(){
console.info(name);
};
})();
person(); // 輸出成功
console.info(person.name); //// 輸出失敗
-
延續(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)過排查發(fā)現(xiàn)撩笆,使用
report()
函數(shù)存在30%丟失數(shù)據(jù)的情況捺球。這是因?yàn)椋?code>img是report()
函數(shù)中的局部變量,函數(shù)執(zhí)行完畢后就被銷毀了夕冲,而這個(gè)時(shí)候往往HTTP請(qǐng)求還沒建立成功氮兵。而通過閉包來保存img
變量可以解決請(qǐng)求丟失的問題:
//注意:我們將普通函數(shù)改成了自執(zhí)行函數(shù)
var report = (function(){
var imgs = [];
return function(dataSrc){
var img = new Image();
images.push(img);
img.src = dataSrc;
}
})();
-
用閉包實(shí)現(xiàn)面向?qū)ο?/strong>:我們經(jīng)常使用
過程
和數(shù)據(jù)
來描述面向?qū)ο缶幊坍?dāng)中的對(duì)象。對(duì)象的方法包含了過程歹鱼,而閉包則是在過程中以執(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"
- 用閉包實(shí)現(xiàn)命令模式
- 命令模式是將請(qǐng)求封裝成對(duì)象疼电,從而可以把不同的請(qǐng)求對(duì)象進(jìn)行參數(shù)化嚼锄、對(duì)請(qǐng)求對(duì)象排隊(duì)或者記錄日志以及執(zhí)行可撤銷的操作。
- 命令模式的能夠分離請(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">開啟</button>
<button id="undo">關(guān)閉</button>
<script type="text/javascript">
var Tv = {
open : function(){
console.info("打開電視機(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("打開電視機(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)存管理
- 一直流傳著一種聳人聽聞的說法茫虽,聲稱閉包會(huì)造成內(nèi)存泄漏刊苍,所以應(yīng)當(dāng)盡量避免使用閉包。
- 局部變量本來應(yīng)該在函數(shù)退出的時(shí)候被釋放濒析,但在閉包形成的環(huán)境中正什,局部變量不被釋放。從這個(gè)意義上看号杏,確實(shí)會(huì)造成一些數(shù)據(jù)無法被及時(shí)銷毀婴氮。但我們使用閉包斯棒,是我們主動(dòng)選擇延長(zhǎng)局部變量的生命周期,不能說成是內(nèi)存泄漏主经。當(dāng)使用完畢后荣暮,大可手動(dòng)將這些變量設(shè)為
null
。 - 而只有閉包形成循環(huán)引用的情況下罩驻,才會(huì)導(dǎo)致內(nèi)存泄漏穗酥。但這也不是閉包或者JavaScript的問題,我們可以避免循環(huán)引用的情況惠遏,而不是因噎廢食砾跃,徹底摒棄閉包。
3.2 高階函數(shù)
-
高階函數(shù)
是指滿足以下兩個(gè)條件之一的函數(shù):
- 函數(shù)可以作為參數(shù)被傳遞节吮;
- 函數(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)聽事件
$("btn").click(function(){
console.info("btn clicked");
});
//可以發(fā)現(xiàn),其本質(zhì)就是執(zhí)行了click()方法方仿,然后傳入一個(gè)函數(shù)作為參數(shù)固棚。
//注意到:在按鈕點(diǎn)擊后的處理是變化的,通過回調(diào)函數(shù)來封裝變化仙蚜。
- 另外還有
Array.sort()
此洲。這是用來排序數(shù)組的一個(gè)方法,傳入一個(gè)自定義的函數(shù)來指定是遞增還是遞減排序委粉。
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)接觸過了,這使得整個(gè)運(yùn)算過程是可延續(xù)贾节。
- 我們通過優(yōu)化一段類型判斷的JavaScript代碼來感受函數(shù)作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
//通過傳入的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部分都是相同的,我們通過將函數(shù)作為返回值的方式優(yōu)化代碼栗涂。
//抽象出一個(gè)類型判斷的通用函數(shù)
var isType = funcion(type){
//該函數(shù)返回一個(gè)可執(zhí)行的函數(shù)知牌,用來執(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ù)邏輯無關(guān)的模塊代碼獨(dú)立出來扁藕,通過“動(dòng)態(tài)植入”的方式參入到業(yè)務(wù)邏輯模塊當(dāng)中沮峡。這樣可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性。
- Java語(yǔ)言可以通過反射和動(dòng)態(tài)代理機(jī)制來實(shí)現(xiàn)AOP技術(shù)亿柑,而JavaScript函數(shù)作為返回值的特性就可以簡(jiǎn)單的實(shí)現(xiàn)邢疙,這是JavaScript與生俱來的能力。
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ā)展起來的式矫,所以因此得名乡摹。
- currying又稱為
部分求值
。currying函數(shù)首先接受一些參數(shù),接受這些參數(shù)之后并不立即求值,而是返回另外一個(gè)函數(shù),并將傳入的參數(shù)函數(shù)保存起來.等真正需要求值的時(shí)候,將之前傳入的所有參數(shù)一次性的求值. - 我們通過JavaScript,通過一個(gè)記賬的代碼來模擬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ù)組中緩存起來
[].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)反柯里化
- 通過
call()
和apply()
方法可以借用別的對(duì)象的方法,比如借用Array.prototype.push()
方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個(gè)問題的.
//為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)致性能問題察绷。
- 比如用來監(jiān)聽瀏覽器窗口大小的
window.onresize
事件,當(dāng)瀏覽器窗口被不斷拉伸時(shí),這個(gè)事件觸發(fā)的頻率會(huì)非常高;又比如元素的拖拽監(jiān)聽事件onmousemove
,如果元素被不停的拖拽,也會(huì)頻繁的觸發(fā)干签;還有最典型的監(jiān)聽文件上傳進(jìn)度的事件,由于需要不斷掃描文件用以在頁(yè)面中顯示掃描進(jìn)度拆撼。導(dǎo)致通知的頻率非常之高容劳,大約一秒鐘10次,遠(yuǎn)超過人眼所能覺察的極限闸度。 - throttle函數(shù)就是解決此類問題的方案竭贩。
throttle
顧名思義節(jié)流器,借鑒的是工程學(xué)里的思想,比如用節(jié)流器來穩(wěn)定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時(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);
- 通過分時(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ù)的特性實(shí)現(xiàn)惰性加載函數(shù)的案例浩峡,考慮到文章篇幅的關(guān)系孝赫,這里就不贅述了。