查看更多詳情歡迎訪問我的博客 或 訪問我的github 或 我的筆記站萎坷,選擇您喜歡的風格自由馳騁知識的海洋凹联。
高階函數(shù),是指可以作為參數(shù)傳遞或者可以作為返回值輸出的函數(shù)哆档。在JavaScript中蔽挠,函數(shù)是第一類對象,其既可以作為參數(shù)傳遞虐呻,也可以作為其他函數(shù)的返回值輸出象泵,顯然是高階函數(shù)。
高階函數(shù)之參數(shù)傳遞
把函數(shù)當作參數(shù)傳遞斟叼,最常見的要數(shù)回掉函數(shù)偶惠,,我們常常在回掉函數(shù)中存放的是代碼中易變的邏輯部分
回調(diào)函數(shù)
在JavaScript的學習和開發(fā)過程中朗涩,回調(diào)函數(shù)的使用率可謂尤其高忽孽,在DOM事件注冊事件處理程序,ajax異步請求等等諸多場景頻繁使用回調(diào)函數(shù)谢床。
/**
* [addHandler 處理事件注冊兼容]
* @param {[type]} elem [dom元素]
* @param {[type]} type [事件類型]
* @param {[type]} handler [事件處理程序]
*/
function addHandler(elem, type, handler) {
if (document.addEventListener) {
elem.addEventListener(type, handler);
}else if (document.attachEvent) {
elem.attachEvent('on' + type, handler);
}else {
elem['on' + type] = handler;
}
}
function fn1(e) {
console.log(e);
}
var div = document.getElementById('myDiv');
addHandler(div, 'click', fn1);
又比如Array.prototype.sort方法兄一,接受一個函數(shù)當作參數(shù),函數(shù)里面封裝的是對數(shù)組進行排序的規(guī)則识腿。
var arr = [1, 3, 5, 7, 2, 4, 6];
arr.sort(function(a, b) {
return a - b; //從小到大排序出革,如需從大到小排序,只需return b - a;
});
高階函數(shù)之返回值輸出
相信大家對函數(shù)式編程一定不陌生渡讼,至少都耳熟過骂束,,在函數(shù)式編程中成箫,將函數(shù)當作返回值輸出很是常見展箱。
類型檢測
JavaScript是一門動態(tài)的腳本語言,其數(shù)據(jù)類型直到程序運行時才能確定蹬昌,于是在JavaScript中對數(shù)據(jù)類型進行檢測判斷不如Java等其他靜態(tài)語言來的簡便直接混驰。
typeof和instanceof
JavaScript中,常見的兩個判斷數(shù)據(jù)類型的方法是使用typeof和instanceof,在大多數(shù)情況下還是能正確判斷出數(shù)據(jù)類型的,但這兩者都有其局限性皂贩,因此栖榨,其使用也必然有局限性。
console.log(typeof null); //object
var arr = [];
console.log(arr instanceof Array);//true
function test() {
console.log(arguments instanceof Array); //輸出false
}
test();
只有當arr是數(shù)組明刷,且與Array構(gòu)造函數(shù)在同一全局作用域中婴栽,arr instanceof Array才返回true,如果arr是某一frame中定義的數(shù)組遮精,上式返回false居夹。
Object.prototype.toString
Object.prototype.toString方法總是返回字符串败潦,如Object.prototype.toString.call([1,2,3])總是返回"[object Array]",Object.prototype.toString.call('str')總是返回"[object String]",Object.prototype.toString.call(12)總是返回"[object Number]",所以可以使用Object.prototype.
更多關(guān)于calls與apply可查看本系列筆記篇JavaScript之this,call與apply
var isType = function(type) {
return function(obj) {
return Object.prototype.toString.call(obj) == '[object ' + type + ']';
}
}
var isString = isType('String');
console.log(isString('testString')); //輸出true
var isArray = isType('Array');
console.log(isArray([1,3,5])); //輸出true
作用域安全的構(gòu)造函數(shù)
構(gòu)造函數(shù)准脂,即是一個函數(shù)劫扒,通過new操作符調(diào)用,以new操作符調(diào)用時狸膏,構(gòu)造函數(shù)內(nèi)部this指向新創(chuàng)建的構(gòu)造函數(shù)實例對象沟饥。
function Animal(name, type) {
this.name = name;
this.type = type;
}
var animal1 = new Animal('wangwang', 'Dog');
console.log(animal1.name, animal1.type); //輸出wnagwnag DOg
var animal2 = Animal('miaomiao', 'Cat');
console.log(window.name, window.type); //輸出miaomiao Cat
console.log(animal2.name, animal2.type); //TypeError: Cannot read property 'name' of undefined
當使用new操作符調(diào)用Animal構(gòu)造函數(shù)時,會創(chuàng)建一個新的Animal對象湾戳,此對象擁有name和type屬性贤旷;當直接調(diào)用Animal構(gòu)造函數(shù)時,this對象指向全局環(huán)境即window(普通函數(shù)方式調(diào)用時砾脑,this總是指向全局環(huán)境幼驶,更多關(guān)于this指向問題可查看本系列筆記篇JavaScript之this,call與apply,于是上述代碼window上有name和type屬性韧衣,animal2上沒有盅藻。
上述問題該如何解決呢?既然問題源自this,那就從this著手:
function Animal(name, type) {
if (this instanceof Animal) {
this.name = name;
this.type = type;
}else {
return new Animal(name, type);
}
}
var animal1 = new Animal('wangwang', 'Dog');
console.log(animal1.name, animal1.type); //輸出wnagwnag DOg
var animal2 = Animal('miaomiao', 'Cat');
console.log(window.name); //輸出undefined
console.log(animal2.name, animal2.type); //輸出miaomiao Cat
在使用new調(diào)用構(gòu)造函數(shù)時畅铭,this指向Animal實例氏淑,this instanceof Animal返回true;當直接調(diào)用構(gòu)造函數(shù)時硕噩,this指向全局環(huán)境window假残,this instanceof Animal返回false,此構(gòu)造函數(shù)內(nèi)部處理強制返回一個新實例炉擅,于是animal2中包含name和type屬性辉懒。
函數(shù)綁定
函數(shù)綁定常指在調(diào)用某一函數(shù)時,將此函數(shù)內(nèi)部this指向特定上下文環(huán)境坑资,就是函數(shù)內(nèi)部this和上下文環(huán)境的綁定耗帕。
var Animal = {
name: 'Cat',
getName: function() {
return this.name;
}
};
var Animal2 = {
name: 'Dog'
};
console.log(Animal.getName()); //輸出Cat
var div = document.getElementById('myDiv');
div.addEventListener('mouseover', Animal.getName); //返回undefined
div.addEventListener('click', function(e) {
console.log(this);
console.log(Animal.getName()); //輸出Cat
});
如上代碼中第一個輸出Cat穆端,很好理解袱贮,this指向Animal;而第二個返回undefined是為什么呢,因為在給DOM元素注冊事件處理程序時体啰,會修改事件處理程序的this指向攒巍,使其指向當前DOM元素(IE8除外,IE8中指向全局環(huán)境window)荒勇;第三處輸出Cat是因為DOM事件修改的是事件處理程序的this指向柒莉,即此處Animal.getName()的包含環(huán)境中的this,此包含函數(shù)形成一個閉包沽翔,在此閉包中直接調(diào)用Animal.getName()兢孝,getName函數(shù)里面this指向Animal窿凤。
我們再看一段代碼:
Function.prototype.bind = function(context) {
var self = this;
return function() {
self.apply(context, arguments);
};
};
var Animal = {
name: 'Cat',
getName: function() {
//console.log(this.name);
return this.name;
}
};
var Animal2 = {
name: 'Dog'
};
console.log(Animal.getName.bind(Animal2)()); //輸出DOg
var ani = Animal.getName.bind(Animal);
console.log(ani(1,2,3)); //輸出Cat,此時bind方法返回的函數(shù)內(nèi)部的arguments屬性就是一個包含1,2,3的類數(shù)組
var div = document.getElementById('myDiv');
div.addEventListener('click', Animal.getName.bind(Animal));
//點擊后返回Cat
我們在Function原型上定義了bind函數(shù),其返回一個函數(shù)跨蟹,這個函數(shù)形成一個閉包雳殊,其this與其包含函數(shù)this并不指向同一環(huán)境,于是使用了self保存包含函數(shù)的this對象窗轩,self便指向調(diào)用bind方法的函數(shù)夯秃,然后又使用apply調(diào)用此函數(shù),傳入了context上下文環(huán)境痢艺,及內(nèi)部閉包函數(shù)的參數(shù)仓洼。
注:在調(diào)用setTimeout,setInterval時,其內(nèi)部this總是指向全局環(huán)境window堤舒,因此定時器函數(shù)中被調(diào)用的函數(shù)經(jīng)常需要我們手動為其綁定上下文環(huán)境色建。
var obj = {
test: function() {
console.log(this); //輸出object{test: function},即指向obj
setTimeout(function() {
console.log(this === window); //輸出true
},300);
}
}
obj.test();
函數(shù)柯里化
currying
函數(shù)柯里化(currying)又叫部分求值。一個currying的函數(shù)接收一些參數(shù)舌缤,接收了這些參數(shù)之后镀岛,該函數(shù)并不是立即求值,而是繼續(xù)返回另一個函數(shù)友驮,剛才傳入的參數(shù)在函數(shù)形成的閉包中被保存起來漂羊,待到函數(shù)真正需要求值的時候,之前傳入的所有參數(shù)都能用于求值卸留。
var totalCost = 0;
var cost = function(money) {
totalCost += money;
};
cost(100);
cost(200);
cost(400);
cost(800);
console.log(totalCost); //輸出1500
如上累計計算totalCost值走越,每次都調(diào)用cost方法,假如我們只需保存前幾次的增加值耻瑟,在最后一次才求值旨指,如何實現(xiàn)為好呢?可以用currying函數(shù)化思想簡單實現(xiàn):
var cost = (function() {
var args = [];
return function() {
if (!arguments.length) {
var num = 0;
for (var i = 0, l = args.length; i < l; i++) {
num += args[i];
}
return num;
}else {
[].push.apply(args, arguments);
}
}
})();
cost(100);
cost(200);
cost(400);
cost(800);
console.log(cost()); //輸出1500
上述代碼每次調(diào)用cost傳入?yún)?shù)時并未真正進行計算喳整,而只是把參數(shù)保存在cost函數(shù)所形成的閉包中谆构,在其返回函數(shù)中可隨時訪問進行操作,最后需要進行求值時只需要以無參數(shù)形式調(diào)用cost即可框都。
接下來闡述如何實現(xiàn)一個通用的currying化函數(shù):
var currying = function(fn) {
};
Function.prototype.currying = function() {
var self = this;
var args = []; //保存所有參數(shù)值
return function() { //形成閉包搬素,返回一個新函數(shù),能保存對args的訪問
if (!arguments.length) {
return self.apply(this, args); //調(diào)用currying化函數(shù)魏保,并把args作為參數(shù)傳入熬尺。
}else {
[].push.apply(args, arguments); //將每次傳入的新參數(shù)存入args
return arguments.callee;
}
}
};
var cost = (function() {
var num = 0;
return function() {
num = 0; //確保每次調(diào)用cost()時重新計算所有已保存值值
for (var i = 0, l = arguments.length; i < l; i++) {
num += arguments[i]; //對所有參數(shù)求和
}
return num;
}
})();
var cost = cost.currying();
cost(100);
cost(200);
cost(400);
console.log(cost()); //輸出700
cost(800);
console.log(cost()); //輸出1500
如上實現(xiàn)了一個currying函數(shù),調(diào)用cost時明確傳入了參數(shù)谓罗,則此時只是保存參數(shù)值粱哼,不進行業(yè)務(wù)計算拐袜,cost返回另一個函數(shù)剪决。在不傳參數(shù)調(diào)用cost時斑唬,對之前所有保存的參數(shù)值進行業(yè)務(wù)計算完慧。
注:currying化函數(shù)后調(diào)用的cost都是currying函數(shù)cost,而非原本的cost函數(shù)绊含。
uncurrying
在JavaScript學習和開發(fā)過程中需纳,我們經(jīng)常需要讓某對象去調(diào)用一個原本不屬于自己的方法,如Array.prototype.push或是其他自定義對象上的方法艺挪,一般我們經(jīng)常使用的是apply與call,更多有關(guān)apply與call請訪問本系列篇JavaScript之this,call與apply.
var obj = {
name: 'Dog',
getName: function() {
return this.name;
}
};
var obj2 = {
name: 'Cat'
};
console.log(obj.getName.call(obj2)); //輸出Cat
在本節(jié)不翩,我們要使用uncurrying來實現(xiàn)通用的方法以實現(xiàn)需求:
Function.prototype.uncurrying = function() {
var self = this;
return function() {
var obj = Array.prototype.shift.call(arguments);
return self.apply(obj, arguments);
};
};
var push = Array.prototype.push.uncurrying();
(function() {
push(arguments, 7);
console.log(arguments); //輸出[1,3,5,7]
})(1,3,5);
uncurrying使Array.prototype.push方法變成了一個通用的push函數(shù),push即等價于Array.prototype.push.call麻裳。
在調(diào)用Array.prototype.push.uncurrying()時口蝠,self為Array.prototype.push,調(diào)用push(arguments, 7)時,obj為[1,3,5]津坑,uncurrying方法返回函數(shù)內(nèi)部的arguments為后文自執(zhí)行函數(shù)調(diào)用push傳入的7妙蔗,self.apply(obj, arguments)即等價于Array.prototype.push.apply([1,3,5], 7)。
函數(shù)節(jié)流
JavaScript中疆瑰,有某些函數(shù)是可能會被頻繁調(diào)用眉反,從而造成性能問題,甚至出現(xiàn)功能上的偏差穆役。如:
- window.onresize事件寸五,在為window注冊了resize事件后,若頻繁的修改窗口大小耿币,就會頻繁的觸發(fā)resize事件梳杏,會非常消耗性能,容易造成瀏覽器卡頓甚至崩潰淹接。
- mouseenter十性,mouseleave,mousemove等事件塑悼,這些事件由鼠標劃入劲适,劃出,移動觸發(fā)厢蒜,其觸發(fā)頻率可能非诚际疲快,特別是當我們在這是事件處理程序中定義了動畫處理時郭怪,問題尤其需要注意支示,我們知道刊橘,每一段動畫一般都有一個執(zhí)行周期鄙才,多個動畫都是壓入一個動畫隊列,一次執(zhí)行促绵,假如我們觸發(fā)事件的頻率非吃茆郑快嘴纺,動畫將重復觸發(fā)。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>無標題文檔</title>
<script src="./js/jquery.min.js">
</script>
</head>
<body>
<div class="a1" style="width:100px; height:100px; background-color:#000;"></div>
<div class="a2" style="width:1000px; height:200px; background-color:#F00; display:none;"></div>
<div class="b1" style="width:100px; height:100px; background-color:#000;margin-top:20px;"></div>
<div class="b2" style="width:1000px; height:200px; background-color:#F00; display:none;"></div>
<script type="text/javascript">
$('.a1').mouseenter(function(){
$('.a2').fadeIn();
});
$('.a1').mouseleave(function(){
$('.a2').fadeOut();
});
$('.b1').mouseenter(function(){
$('.b2').fadeIn();
});
var timeoutId = null;
$('.b1').mouseleave(function(){
clearTimeout(timeoutId);
timeoutId = setTimeout(function(){
$('.b2').fadeOut();
}, 500);
});
</script>
</body>
</html>
代碼演示地址
如上浓冒,當我們快速多次劃過第一個小黑塊時栽渴,其動畫會重復執(zhí)行,在我們的鼠標動作停止后稳懒,還在進行動畫闲擦;而對于第二個小黑塊,則不會發(fā)生這種現(xiàn)象场梆。因為在第二個黑塊的mouseleave事件中設(shè)置了定時器墅冷,并且在每次動畫之前都清除之前的定時器,保證了最后只會有一個定時器或油,即一次動畫被執(zhí)行寞忿。
接下來,我們實現(xiàn)一個通用的節(jié)流函數(shù):
var throttle = function(fn, context, interval) {
clearTimeout(fn.tId);
fn.tId = setTimeout(function() {
fn.apply(context || null);
}, interval || 500);
};
將之前第一個黑塊的mouseleave事件修改成如下即可實現(xiàn)第二個黑塊的函數(shù)節(jié)流效果:
var aFn = function() {
$('.a2').fadeOut();
};
$('.a1').mouseleave(function(){
throttle(aFn);
});
分時函數(shù)
上節(jié)中顶岸,講述了函數(shù)節(jié)流方式用以解決某些函數(shù)被頻繁調(diào)用的問題腔彰,然而有些函數(shù)也會被重復調(diào)用,但這些函數(shù)執(zhí)行次數(shù)卻不能少辖佣,如何在不減少其執(zhí)行次數(shù)的前提下霹抛,把其對頁面性能的破壞降到最低呢?函數(shù)節(jié)流顯然不能解決這個問題卷谈,聰明的程序員提出了分時函數(shù)這一解決方法上炎。
現(xiàn)在企業(yè)內(nèi)部都很流行webIM,企業(yè)即時通訊雏搂,在打開頁面時藕施,需要加載員工列表,這個列表常常會是成百上千的凸郑,一個好友用一個div節(jié)點顯式的話裳食,那就意味著需要創(chuàng)建成百上千個節(jié)點并添加到頁面document,在短時間添加如此多節(jié)點顯然是會影響瀏覽器頁面性能的芙沥,如下:
var arr = [];
for (var i = 1; i < 1000; i ++) {
arr.push(i);
}
var renderStaff = function(data) {
for (var i = 0, l = data.length; i < l; i++) {
var div = document.createElement('div');
div.innerHTML = i;
document.body.appendChild(div);
}
};
renderStaff(arr);
解決問題的方法之一就是使用分時函數(shù)诲祸,如下b把1000個節(jié)點分批進行:
var timeChunk = function(arr, fn, count) {
var obj, t;
var len = arr.length;
var start = function() {
for (var i = 0; i < Math.min(count || 1, arr.length); i ++) {
var obj = arr.shift();
fn(obj);
}
};
return function() {
t = setInterval(function() {
if (!arr.length) {
return clearInterval(t);
}
start();
}, 200);
};
};
于是之前加載1000節(jié)點的代碼重構(gòu)如下,分批創(chuàng)建節(jié)點而昨,每隔200ms創(chuàng)建8個節(jié)點timeChunk接收三個參數(shù)救氯,第一個為節(jié)點數(shù)據(jù),第二個為創(chuàng)建節(jié)點業(yè)務(wù)邏輯歌憨,第三個為每批所創(chuàng)建節(jié)點的個數(shù):
var arr = [];
for (var i = 1; i < 1000; i ++) {
arr.push(i);
}
var renderStaff = timeChunk(arr, function(data) {
var div = document.createElement('div');
div.innerHTML = i;
document.body.appendChild(div);
}, 8);
renderStaff(arr);
惰性加載函數(shù)
由于瀏覽器間的行為差異着憨,在JavaScript的代碼中,為了達到不同瀏覽器的兼容务嫡,經(jīng)常需要寫入大量判斷語句甲抖,對于執(zhí)行不同代碼塊漆改,比如要實現(xiàn)一個在諸瀏覽器間通用的事件綁定函數(shù)addHandler,可以參考如下代碼實現(xiàn):
var addHandler = function(elem, type, handler) {
if (window.addEventListener) {
return elem.addEventListener(type, handler);
}else if (window.attachEvent) {
return elem.attachEvent('on' + type, handler);
}else {
return elem['on' + type] = handler;
}
}
如上代碼准谚,在每次執(zhí)行時都需要重新做條件判斷挫剑,我們要如何才能做到在每個環(huán)境下只做一次判斷呢?addHandler依然聲明為一個普通函數(shù)柱衔,在第一次進入條件分支時樊破,函數(shù)內(nèi)部會重寫這個函數(shù),重寫的就是符合當前環(huán)境的addHandler函數(shù)唆铐,當再次進入addHandler函數(shù)時捶码,函數(shù)里不再存在條件分支語句:
var addHandler = function(elem, type, handler) {
if (window.addEventListener) {
addHandler = function(elem, type, handler) {
elem.addEventListener(type, handler);
}
}else if (window.attachEvent) {
addHandler = function(elem, type, handler) {
elem.attachEvent('on' + type, handler);
}
}else {
addHandler = function(elem, type, handler) {
elem['on' + type] = handler;
}
}
addHandler(elem, type, handler);
};
//測試一下
var div = document;
addHandler(div, 'click', function() {
console.log('add handler 1');
});
addHandler(div, 'click', function() {
console.log('add handler 2');
});