三、閉包和高階函數
IMG_20170112_004128.jpg
3.1 閉包
3.1.1 變量的作用域
- 所謂變量的作用域,就是變量的有效范圍。通過作用域的劃分,JavaScript變量分為全局變量和局部變量。
- 聲明在函數外的變量為
全局變量
;在函數內并且以var
關鍵字聲明的變量為局部變量
。 - 我們都知道,全局變量能在任何作用域訪問到,但這很容易造成命名沖突;而局部變量只有在函數里面能訪問到,這是因為JavaScript的查找變量的規則是從內往外搜索的。
3.1.2 變量的生命周期
- 全局變量的生命周期是永久的(除非我們主動銷毀這個全局變量),而局部變量則當函數執行完畢時被銷毀。
- 那JavaScript中是否存在,即便函數執行完畢,依然不會被銷毀的局部變量?答案是肯定的。
<script type="text/javascript">
//現在有一個名為func的函數
var func = function(){
//①函數執行體中,將局部變量a賦值為1
var a = 1;
//②返回一個function執行環境
return function(){
//③執行環境中,將func.a局部變量加1,然后輸出到控制臺
a++;
console.info(a);
}
};
//調用:將func函數執行后的返回,賦值給f
var f = func();
f(); //f()調用一次,輸出2
f(); //f()再調用一次,輸出3
f(); //f()接著調用,輸出4
</script>
- 以上案例中的
func.a
局部變量,在func()
函數執行過后并沒有被銷毀。每次執行f()
時,仍能對它進行累加,就是佐證。這是因為func()
返回了一個匿名函數的引用賦值給f
,正是由于被外部變量引用了,所以不被銷毀,此時這個匿名函數就稱為閉包
。 - 什么是閉包?
在一個函數內定義另外一個函數(內部函數可以訪問外部函數的變量),如果將這個內部函數提供給其他變量引用時,內部函數作用域以及依賴的外部作用域的執行環境就不會被銷毀。此時這個內部函數就像一個可以訪問封閉數據包的執行環境,也就是閉包。
3.1.3 閉包的用途
- 我們不但要學習什么是JavaScript閉包,更要了解如何利用閉包特性來寫代碼。由于篇幅有限,書中只羅列了幾個使用閉包的例子,但要知道實際開發中運用閉包非常廣泛,遠不止于此。
- 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
var name = "William";
return function(){
console.info(name);
};
})();
person(); // 輸出成功
console.info(person.name); //// 輸出失敗
-
延續變量的生命周期:我們經常用
<img>
標簽進行數據上報,創建一個臨時的img標簽,將需要上報的數據附加在img的url后綴,從而上送到服務器。如例子所示:
var report = function(dataSrc){
var img = new Image(); //創建image對象
img.src = dataSrc; //將要上送的數據url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
- 可經過排查發現,使用
report()
函數存在30%丟失數據的情況。這是因為,img
是report()
函數中的局部變量,函數執行完畢后就被銷毀了,而這個時候往往HTTP請求還沒建立成功。而通過閉包來保存img
變量可以解決請求丟失的問題:
//注意:我們將普通函數改成了自執行函數
var report = (function(){
var imgs = [];
return function(dataSrc){
var img = new Image();
images.push(img);
img.src = dataSrc;
}
})();
-
用閉包實現面向對象:我們經常使用
過程
和數據
來描述面向對象編程當中的對象。對象的方法包含了過程,而閉包則是在過程中以執行環境的方式包含了數據。
- 既然閉包可以封裝私有變量,自然也能完成面向對象的設計。實際上,用面向對象思想能實現的功能,用閉包也能實現,反之亦然,這就是JavaScript的靈活之處。
- 有這樣一段面向對象的JS代碼:
//Person構造器,里面有一個name屬性
var Person = function(){
this.name = "William";
};
//給Person的原型添加一個sayName()方法
Person.prototype.sayName = function(){
console.info("hello,my name is " + this.name);
};
//實例化Person
var person1 = new Person();
person1.sayName();
- 用閉包可以實現同樣的效果:因為在JavaScript用new執行構造函數,本質也是返回一個對象
//person()函數返回一個有sayName()方法的對象
var person = function(){
var name = "William";
return {
sayName : function(){
console.info("hello,my name is " + name);
}
}
};
//執行person()函數,將返回的對象賦值給person1
var person1 = person();
//調用person1.sayName()方法
person1.sayName();
//控制臺輸出 "hello,my name is William"
- 用閉包實現命令模式
- 命令模式是將請求封裝成對象,從而可以把不同的請求對象進行參數化、對請求對象排隊或者記錄日志以及執行可撤銷的操作。
- 命令模式的能夠分離請求發起者和執行者之間的耦合關系。往往在命令被執行之前,就預先往命令對象中植入命令的執行者。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<button id="execute">開啟</button>
<button id="undo">關閉</button>
<script type="text/javascript">
var Tv = {
open : function(){
console.info("打開電視機");
},
close : function(){
console.info("關閉電視機");
}
};
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();
}
};
//調用
setCommand(new OpenTvCommand(Tv));
</script>
</body>
</html>
- 用閉包實現命令模式:
<script type="text/javascript">
var Tv = {
open : function(){
console.info("打開電視機");
},
close : function(){
console.info("關閉電視機");
}
};
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();
}
};
//調用
setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內存管理
- 一直流傳著一種聳人聽聞的說法,聲稱閉包會造成內存泄漏,所以應當盡量避免使用閉包。
- 局部變量本來應該在函數退出的時候被釋放,但在閉包形成的環境中,局部變量不被釋放。從這個意義上看,確實會造成一些數據無法被及時銷毀。但我們使用閉包,是我們主動選擇延長局部變量的生命周期,不能說成是內存泄漏。當使用完畢后,大可手動將這些變量設為
null
。 - 而只有閉包形成循環引用的情況下,才會導致內存泄漏。但這也不是閉包或者JavaScript的問題,我們可以避免循環引用的情況,而不是因噎廢食,徹底摒棄閉包。
3.2 高階函數
-
高階函數
是指滿足以下兩個條件之一的函數:
- 函數可以作為參數被傳遞;
- 函數可以作為返回值輸出;
- 顯然,JavaScript語言中的函數兩個條件都滿足,下面將講解JavaScript高階函數特性的應用示例。。
3.2.1 函數作為參數傳入
- 把函數當做參數傳遞,使得我們可以抽離出一部分容易變化的業務邏輯。
- 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定,或者jQuery中的ajax請求:
//按鈕監聽事件
$("btn").click(function(){
console.info("btn clicked");
});
//可以發現,其本質就是執行了click()方法,然后傳入一個函數作為參數。
//注意到:在按鈕點擊后的處理是變化的,通過回調函數來封裝變化。
- 另外還有
Array.sort()
。這是用來排序數組的一個方法,傳入一個自定義的函數來指定是遞增還是遞減排序。
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 函數作為返回值輸出
- 讓函數返回一個可執行的函數,在之前的代碼我們已經接觸過了,這使得整個運算過程是可延續。
- 我們通過優化一段類型判斷的JavaScript代碼來感受函數作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
//通過傳入的obj對象執行toString()方法,將結果值和預期字符串比較
return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數組
var isArray = function(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數字
var isNumber = function(obj){
return Object.prototype.toString.call(obj) === '[object Number]';
}
- 可以發現上面的代碼toString部分都是相同的,我們通過將函數作為返回值的方式優化代碼。
//抽象出一個類型判斷的通用函數
var isType = funcion(type){
//該函數返回一個可執行的函數,用來執行toString方法和預期字符串做比較
return function(obj){
return Object.prototype.toString.call(obj) === '[Object '+type+']';
}
}
//預先注冊具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調用
console.info(isArray([1,3,2])); //輸出: true
- 另外一個例子,是利用JavaScript函數作為返回值這個特性實現單例模式。
var getSingle = function(fn){
var ret; //臨時變量
return function(){
//如果ret已經存在的話則返回;否則新創建對象
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 高階函數實現AOP
- AOP(面向切面編程)是指將日志統計、安全控制、異常處理等與業務邏輯無關的模塊代碼獨立出來,通過“動態植入”的方式參入到業務邏輯模塊當中。這樣可以保持業務邏輯模塊的純凈和高內聚性。
- Java語言可以通過反射和動態代理機制來實現AOP技術,而JavaScript函數作為返回值的特性就可以簡單的實現,這是JavaScript與生俱來的能力。
Function.prototype.invokeBefore = function(beforFn){
var _self = this; //原函數的引用
return function(){
//先執行傳入的before函數
beforFn.apply(this,arguments);
//然后再執行自身
return _self.apply(this,arguments);
}
}
Function.prototype.invokeAfter = function(afterFn){
var _self = this; //
return function(){
//先執行函數,并保存執行結果
var ret = _self.apply(this,arguments);
//然后再執行after函數
afterFn.apply(this,arguments);
//最后返回結果
return ret;
}
}
//定義一個方法,控制臺輸出2
var func = function(){
console.info(2);
};
//指定func()函數執行前和執行后要做的事情
func = func.invokeBefore(function(){
console.info(1);
}).invokeAfter(function(){
console.info(3);
});
//調用func()函數,控制臺輸出 1 2 3
func();
3.2.4 高階函數實現柯里化
- 函數柯里化(function currying)的概念是由注明數理邏輯學家Haskell Curry豐富和發展起來的,所以因此得名。
- currying又稱為
部分求值
。currying函數首先接受一些參數,接受這些參數之后并不立即求值,而是返回另外一個函數,并將傳入的參數函數保存起來.等真正需要求值的時候,將之前傳入的所有參數一次性的求值. - 我們通過JavaScript,通過一個記賬的代碼來模擬currying函數
var currying = function(fn){
var args = []; //緩存對象
return function(){
if(arguments.length == 0){
//如果傳入的參數為空,則直接返回結果
return fn.apply(this,args);
}else{
//如果參數不為空,則將傳入參數push到args數組中緩存起來
[].push.apply(args,arguments);
//并返回函數本身
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;
}
});
//轉換成currying函數
var cost = currying(cost);
cost(100); //記賬100,未真正求值
cost(100); //記賬100,未真正求值
cost(400); //記賬400,未真正求值
console.info(cost()); //求值,并輸出:600
3.2.5 高階函數實現反柯里化
- 通過
call()
和apply()
方法可以借用別的對象的方法,比如借用Array.prototype.push()
方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個問題的.
//為Function對象的原型添加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 高階函數實現函數節流
- JavaScript中的函數大多數都是由用戶主動觸發的,尤其在瀏覽器端的某些情況下函數被非常頻繁的調用,從而導致性能問題。
- 比如用來監聽瀏覽器窗口大小的
window.onresize
事件,當瀏覽器窗口被不斷拉伸時,這個事件觸發的頻率會非常高;又比如元素的拖拽監聽事件onmousemove
,如果元素被不停的拖拽,也會頻繁的觸發;還有最典型的監聽文件上傳進度的事件,由于需要不斷掃描文件用以在頁面中顯示掃描進度。導致通知的頻率非常之高,大約一秒鐘10次,遠超過人眼所能覺察的極限。 - throttle函數就是解決此類問題的方案。
throttle
顧名思義節流器,借鑒的是工程學里的思想,比如用節流器來穩定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時間內函數的密集執行,達到穩定性能的作用。
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 高階函數實現分時函數
- 函數節流是限制函數被頻繁調用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時也影響著頁面的性能。比如WebQQ加載好友列表,往往需要短時間內一次性創建成百上千個節點,嚴重影響頁面性能。
//模擬添加1000個數據
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);
- 通過分時函數讓創建節點的工作分批進行。
//創建timeChunk函數
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);
}
};
//測試
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);
- 除此之外,書中還有通過高階函數的特性實現惰性加載函數的案例,考慮到文章篇幅的關系,這里就不贅述了。
3.1 閉包
3.1.1 變量的作用域
- 所謂變量的作用域,就是變量的有效范圍。通過作用域的劃分,JavaScript變量分為全局變量和局部變量。
- 聲明在函數外的變量為
全局變量
;在函數內并且以var
關鍵字聲明的變量為局部變量
。 - 我們都知道,全局變量能在任何作用域訪問到,但這很容易造成命名沖突;而局部變量只有在函數里面能訪問到,這是因為JavaScript的查找變量的規則是從內往外搜索的。
3.1.2 變量的生命周期
- 全局變量的生命周期是永久的(除非我們主動銷毀這個全局變量),而局部變量則當函數執行完畢時被銷毀。
- 那JavaScript中是否存在,即便函數執行完畢,依然不會被銷毀的局部變量?答案是肯定的。
<script type="text/javascript">
//現在有一個名為func的函數
var func = function(){
//①函數執行體中,將局部變量a賦值為1
var a = 1;
//②返回一個function執行環境
return function(){
//③執行環境中,將func.a局部變量加1,然后輸出到控制臺
a++;
console.info(a);
}
};
//調用:將func函數執行后的返回,賦值給f
var f = func();
f(); //f()調用一次,輸出2
f(); //f()再調用一次,輸出3
f(); //f()接著調用,輸出4
</script>
- 以上案例中的
func.a
局部變量,在func()
函數執行過后并沒有被銷毀。每次執行f()
時,仍能對它進行累加,就是佐證。這是因為func()
返回了一個匿名函數的引用賦值給f
,正是由于被外部變量引用了,所以不被銷毀,此時這個匿名函數就稱為閉包
。 - 什么是閉包?
在一個函數內定義另外一個函數(內部函數可以訪問外部函數的變量),如果將這個內部函數提供給其他變量引用時,內部函數作用域以及依賴的外部作用域的執行環境就不會被銷毀。此時這個內部函數就像一個可以訪問封閉數據包的執行環境,也就是閉包。
3.1.3 閉包的用途
- 我們不但要學習什么是JavaScript閉包,更要了解如何利用閉包特性來寫代碼。由于篇幅有限,書中只羅列了幾個使用閉包的例子,但要知道實際開發中運用閉包非常廣泛,遠不止于此。
- 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
var name = "William";
return function(){
console.info(name);
};
})();
person(); // 輸出成功
console.info(person.name); //// 輸出失敗
-
延續變量的生命周期:我們經常用
<img>
標簽進行數據上報,創建一個臨時的img標簽,將需要上報的數據附加在img的url后綴,從而上送到服務器。如例子所示:
var report = function(dataSrc){
var img = new Image(); //創建image對象
img.src = dataSrc; //將要上送的數據url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
- 可經過排查發現,使用
report()
函數存在30%丟失數據的情況。這是因為,img
是report()
函數中的局部變量,函數執行完畢后就被銷毀了,而這個時候往往HTTP請求還沒建立成功。而通過閉包來保存img
變量可以解決請求丟失的問題:
//注意:我們將普通函數改成了自執行函數
var report = (function(){
var imgs = [];
return function(dataSrc){
var img = new Image();
images.push(img);
img.src = dataSrc;
}
})();
-
用閉包實現面向對象:我們經常使用
過程
和數據
來描述面向對象編程當中的對象。對象的方法包含了過程,而閉包則是在過程中以執行環境的方式包含了數據。
- 既然閉包可以封裝私有變量,自然也能完成面向對象的設計。實際上,用面向對象思想能實現的功能,用閉包也能實現,反之亦然,這就是JavaScript的靈活之處。
- 有這樣一段面向對象的JS代碼:
//Person構造器,里面有一個name屬性
var Person = function(){
this.name = "William";
};
//給Person的原型添加一個sayName()方法
Person.prototype.sayName = function(){
console.info("hello,my name is " + this.name);
};
//實例化Person
var person1 = new Person();
person1.sayName();
- 用閉包可以實現同樣的效果:因為在JavaScript用new執行構造函數,本質也是返回一個對象
//person()函數返回一個有sayName()方法的對象
var person = function(){
var name = "William";
return {
sayName : function(){
console.info("hello,my name is " + name);
}
}
};
//執行person()函數,將返回的對象賦值給person1
var person1 = person();
//調用person1.sayName()方法
person1.sayName();
//控制臺輸出 "hello,my name is William"
- 用閉包實現命令模式
- 命令模式是將請求封裝成對象,從而可以把不同的請求對象進行參數化、對請求對象排隊或者記錄日志以及執行可撤銷的操作。
- 命令模式的能夠分離請求發起者和執行者之間的耦合關系。往往在命令被執行之前,就預先往命令對象中植入命令的執行者。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<button id="execute">開啟</button>
<button id="undo">關閉</button>
<script type="text/javascript">
var Tv = {
open : function(){
console.info("打開電視機");
},
close : function(){
console.info("關閉電視機");
}
};
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();
}
};
//調用
setCommand(new OpenTvCommand(Tv));
</script>
</body>
</html>
- 用閉包實現命令模式:
<script type="text/javascript">
var Tv = {
open : function(){
console.info("打開電視機");
},
close : function(){
console.info("關閉電視機");
}
};
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();
}
};
//調用
setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內存管理
- 一直流傳著一種聳人聽聞的說法,聲稱閉包會造成內存泄漏,所以應當盡量避免使用閉包。
- 局部變量本來應該在函數退出的時候被釋放,但在閉包形成的環境中,局部變量不被釋放。從這個意義上看,確實會造成一些數據無法被及時銷毀。但我們使用閉包,是我們主動選擇延長局部變量的生命周期,不能說成是內存泄漏。當使用完畢后,大可手動將這些變量設為
null
。 - 而只有閉包形成循環引用的情況下,才會導致內存泄漏。但這也不是閉包或者JavaScript的問題,我們可以避免循環引用的情況,而不是因噎廢食,徹底摒棄閉包。
3.2 高階函數
-
高階函數
是指滿足以下兩個條件之一的函數:
- 函數可以作為參數被傳遞;
- 函數可以作為返回值輸出;
- 顯然,JavaScript語言中的函數兩個條件都滿足,下面將講解JavaScript高階函數特性的應用示例。。
3.2.1 函數作為參數傳入
- 把函數當做參數傳遞,使得我們可以抽離出一部分容易變化的業務邏輯。
- 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定,或者jQuery中的ajax請求:
//按鈕監聽事件
$("btn").click(function(){
console.info("btn clicked");
});
//可以發現,其本質就是執行了click()方法,然后傳入一個函數作為參數。
//注意到:在按鈕點擊后的處理是變化的,通過回調函數來封裝變化。
- 另外還有
Array.sort()
。這是用來排序數組的一個方法,傳入一個自定義的函數來指定是遞增還是遞減排序。
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 函數作為返回值輸出
- 讓函數返回一個可執行的函數,在之前的代碼我們已經接觸過了,這使得整個運算過程是可延續。
- 我們通過優化一段類型判斷的JavaScript代碼來感受函數作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
//通過傳入的obj對象執行toString()方法,將結果值和預期字符串比較
return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數組
var isArray = function(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數字
var isNumber = function(obj){
return Object.prototype.toString.call(obj) === '[object Number]';
}
- 可以發現上面的代碼toString部分都是相同的,我們通過將函數作為返回值的方式優化代碼。
//抽象出一個類型判斷的通用函數
var isType = funcion(type){
//該函數返回一個可執行的函數,用來執行toString方法和預期字符串做比較
return function(obj){
return Object.prototype.toString.call(obj) === '[Object '+type+']';
}
}
//預先注冊具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調用
console.info(isArray([1,3,2])); //輸出: true
- 另外一個例子,是利用JavaScript函數作為返回值這個特性實現單例模式。
var getSingle = function(fn){
var ret; //臨時變量
return function(){
//如果ret已經存在的話則返回;否則新創建對象
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 高階函數實現AOP
- AOP(面向切面編程)是指將日志統計、安全控制、異常處理等與業務邏輯無關的模塊代碼獨立出來,通過“動態植入”的方式參入到業務邏輯模塊當中。這樣可以保持業務邏輯模塊的純凈和高內聚性。
- Java語言可以通過反射和動態代理機制來實現AOP技術,而JavaScript函數作為返回值的特性就可以簡單的實現,這是JavaScript與生俱來的能力。
Function.prototype.invokeBefore = function(beforFn){
var _self = this; //原函數的引用
return function(){
//先執行傳入的before函數
beforFn.apply(this,arguments);
//然后再執行自身
return _self.apply(this,arguments);
}
}
Function.prototype.invokeAfter = function(afterFn){
var _self = this; //
return function(){
//先執行函數,并保存執行結果
var ret = _self.apply(this,arguments);
//然后再執行after函數
afterFn.apply(this,arguments);
//最后返回結果
return ret;
}
}
//定義一個方法,控制臺輸出2
var func = function(){
console.info(2);
};
//指定func()函數執行前和執行后要做的事情
func = func.invokeBefore(function(){
console.info(1);
}).invokeAfter(function(){
console.info(3);
});
//調用func()函數,控制臺輸出 1 2 3
func();
3.2.4 高階函數實現柯里化
- 函數柯里化(function currying)的概念是由注明數理邏輯學家Haskell Curry豐富和發展起來的,所以因此得名。
- currying又稱為
部分求值
。currying函數首先接受一些參數,接受這些參數之后并不立即求值,而是返回另外一個函數,并將傳入的參數函數保存起來.等真正需要求值的時候,將之前傳入的所有參數一次性的求值. - 我們通過JavaScript,通過一個記賬的代碼來模擬currying函數
var currying = function(fn){
var args = []; //緩存對象
return function(){
if(arguments.length == 0){
//如果傳入的參數為空,則直接返回結果
return fn.apply(this,args);
}else{
//如果參數不為空,則將傳入參數push到args數組中緩存起來
[].push.apply(args,arguments);
//并返回函數本身
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;
}
});
//轉換成currying函數
var cost = currying(cost);
cost(100); //記賬100,未真正求值
cost(100); //記賬100,未真正求值
cost(400); //記賬400,未真正求值
console.info(cost()); //求值,并輸出:600
3.2.5 高階函數實現反柯里化
- 通過
call()
和apply()
方法可以借用別的對象的方法,比如借用Array.prototype.push()
方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個問題的.
//為Function對象的原型添加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 高階函數實現函數節流
- JavaScript中的函數大多數都是由用戶主動觸發的,尤其在瀏覽器端的某些情況下函數被非常頻繁的調用,從而導致性能問題。
- 比如用來監聽瀏覽器窗口大小的
window.onresize
事件,當瀏覽器窗口被不斷拉伸時,這個事件觸發的頻率會非常高;又比如元素的拖拽監聽事件onmousemove
,如果元素被不停的拖拽,也會頻繁的觸發;還有最典型的監聽文件上傳進度的事件,由于需要不斷掃描文件用以在頁面中顯示掃描進度。導致通知的頻率非常之高,大約一秒鐘10次,遠超過人眼所能覺察的極限。 - throttle函數就是解決此類問題的方案。
throttle
顧名思義節流器,借鑒的是工程學里的思想,比如用節流器來穩定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時間內函數的密集執行,達到穩定性能的作用。
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 高階函數實現分時函數
- 函數節流是限制函數被頻繁調用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時也影響著頁面的性能。比如WebQQ加載好友列表,往往需要短時間內一次性創建成百上千個節點,嚴重影響頁面性能。
//模擬添加1000個數據
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);
- 通過分時函數讓創建節點的工作分批進行。
//創建timeChunk函數
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);
}
};
//測試
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);
- 除此之外,書中還有通過高階函數的特性實現惰性加載函數的案例,考慮到文章篇幅的關系,這里就不贅述了。