1 什麼是高階函式
高階函式是指至少要滿足下面條件之一的函式:
- 函式可以作為引數被傳遞
- 函式可以作為返回值輸出
2 函式作為引數傳遞
函式作為引數傳遞,代表我們可以抽離出一部分變化的業務邏輯,將其放入到函式引數中。這樣就可以分離業務程式碼中變化與不變的部分,其中要給重要的場景就是常見的回撥函式。
var getUserInfo = function(userId, callback) {
$.ajax('http://xxx.w.com?', function(data){
if (typeof callback === 'function') {
callback(data);
}
});
}
getUserInfo(12, function(data) {
console.log('user message is :' + data);
})
複製程式碼
3 函式作為返回值輸出
相比把函式當作引數傳遞,函式當作返回值輸出的應用場景也許更多,也更能體現函數語言程式設計的巧妙。讓函式繼續返回一個可執行的函式,意味著運算過程是可延續的。 我們需要對資料型別進行判斷,例如判斷是否是陣列,是否是字串等。
var isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
var isString = function(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}
var isNumber= function(obj) {
return Object.prototype.toString.call(obj) === '[object Number]';
}
複製程式碼
但是實際上上面的內容都重複了,有很多冗餘的程式碼,下面我們將函式座位返回值輸出的方式:
var isType = function(type) {
return function() {
return Object.prototype.toString.call(obj) === `[object ${type}]`
}
};
var isArray = isType(Array);
var isString = isType(String);
var isNumber = isType(Number);
複製程式碼
我們也可以通過迴圈的方式來進行註冊:
var Type = {};
var types = ['String', 'Array', 'Number'];
// map方式已經是閉包的方式,將陣列元素作為引數傳遞
types.map(function(type) {
Type[`is${type}`] = function(obj) {
return Object.prototype.toString.call(obj) === `[object ${type}]`
}
})
// map迴圈等價
for(i = 0; i < types.length; i++) {
var type = types[i];
(function(type) {
Type[`is${type}`] = function(obj) {
console.log(type);
return Object.prototype.toString.call(obj) === `[object ${type}]`
}
})(type);
}
Type.isString('hello');
複製程式碼
4 高階函式實現AOP
AOP(面向切面程式設計)的主要作用是將一些跟核心業務邏輯無關的功能抽離出來,例如日誌統計,安全控制,異常處理等。抽離出來後,再通過動態織入
的方式摻入到業務邏輯模組中。這樣可以保證業務邏輯模組的純淨和高內聚性,其次可以很方便地統計和服用日誌統計功能模組。javascript能夠很方便的實現AOP,這裡通過Function.prototype來實現。這種方式其實是設計模式中的裝飾模式的實現。
Function.prototype.before = function(beforeFunc) {
var _self = this; // 保留原函式的引用,本例子中是testAop
// 返回包含原函式和新函式的'代理'函式
return function() {
console.log('this是window', this);
beforeFunc.apply(this, arguments); // 執行代理函式,修正this
var ret = _self.apply(this, arguments); // 執行原函式(window)
return ret;
}
}
Function.prototype.after = function(afterFunc) {
var _self = this;
return function() {
var ret = _self.apply(this, arguments)
afterFunc.apply(this, arguments);
return ret;
}
}
function beforeFunc() {
console.log('beforeFunc', arguments);
}
function afterFunc() {
console.log('afterFunc', arguments);
}
function testAop(args1, args2) {
console.log('arg1s:' + args1 + ', args2: ' + args2);
}
// testAop.before(beforeFunc)().after(afterFunc)();
var func = testAop.before(beforeFunc).after(afterFunc);
conso
func(10, 20);
複製程式碼
5 高階函式其他使用
5.1 currying(柯里化)
函式柯里化(function currying): 以人名為名稱,currying又稱為部分求值。一個currying的函式會接收一些引數,但是接收這些引數後,不會立即求值,而是繼續返回另外一個函式。例如上面的AOP例子中,剛傳入的引數在函式中形成的閉包儲存了起來,等到函式真正需要求值的時候,再講之前傳入的所有引數被一次性用於求值。 下面使用currying方式完成一個任務:一個月在29日之前都儲存每一天花費了多少錢,在最後一天才進行結算總共花費額度。
var curryingFunc = function(fun) {
console.log(fun);
var moneys = [];
return function() {
// 如果是29日以內,不會傳入計算金額,因此計算花費總額,否則還是返回當前函式,儲存當日花費金額
if (arguments.length === 0) {
// 將money作為引數傳入
return fun.apply(this, moneys);
} else {
[].push.apply(moneys, arguments);
return arguments.callee;
}
};
};
var cost1 = (function() {
var money = 0;
return function() {
return [].reduce.call(arguments, function(a, b) {
return a + b;
}, 0);
};
})();
var cost = curryingFunc(cost1);
cost(10);
cost(20);
cost();
複製程式碼
5.2 uncurrying(反柯里化)
- 柯里化是為了縮小適用範圍,建立一個針對性更強的函式;
- 反柯里化則是擴大適用範圍,建立一個應用範圍更廣的函式。
當我們呼叫物件的某個方法時,不需要關係該物件的設計是否擁有這個方法。這就是動態語言的特點,也就是Duck Typing(鴨子型別)。我們可以通過call, apply借用不屬於物件身上的方法。而通過call, apply方式的話,需要傳入this, 那麼我們應該可以將this提取出來,而這就是uncurrying方式:
Function.prototype.uncurrying = function() {
var _self = this;
return function() {
// 需要呼叫的方法
var obj = Array.prototype.shift.call(arguments);
// 剩下的引數([obj, 10])作為引數傳入call方法中,上下文物件是obj, 引數是10。
// 資料儲存: 10是閉包引數,保持在記憶體中讓obj物件可以訪問。而this.name是使用的obj物件自身的屬性。
_self.apply(obj, arguments)
};
};
複製程式碼
- 通過上面的方法,對Array.push方法進行uncurrying。
var push = Array.prototype.push.uncurrying();
var a = {
0: 1,
1: 2,
length: 2
};
push(a, 3); // {0: 1, 1: 2, 2: 3, length: 3}
複製程式碼
- 通過上面的方法,對call方法進行uncurrying:
var call = Function.prototype.call.uncurrying();
function test(age) {
console.log(`name : ${this.name}, age: ${age}`);
}
var obj = {
name: 'yezi'
}
call(test, obj , 10); // name : yezi, age: 10
複製程式碼
對於call方法的呼叫,其實在uncurrying內部就是這樣的:
Function.prototype.uncurrying = function() {
var _self = this;
return function() {
return Function.prototype.call.apply(_self, arguments)
}
}
複製程式碼
5.3 函式節流
javascript中的函式一般都是使用者自己觸發的,但是有一部分不是由使用者自己控制的。在這些場景下,函式被頻繁地呼叫,會引起大的效能問題。之前我做專案中,在完成地圖的模組:上面顯示很多攻擊資料,而資料是從後端獲取資料口生成的,資料量比較大。當調整瀏覽器視窗的時候,會引起介面重繪,會出現卡頓現象。
- window.onresize(): 觸發的是該方法,當瀏覽器視窗被拖動而改變大小,我們在該方法中進行了DOM節點的相關操作,而DOM節點相關操作是非常損耗效能的。就會出現瀏覽器卡頓而吃不消。
- window.onresize(): 如果給一個div繫結了拖拽事件,當div被拖拽時,也會頻繁觸發該事件
- 上傳進度:有些上傳外掛在瀏覽器中進行真正上傳之前,會對檔案進行掃描並隨時通知javascript函式,以便在頁面中顯示當前的掃描進度。但是該外掛通知的頻率非常高。
函式節流原理: 上面的三個場景它們的共同問題是:函式觸發的頻率太高。例如拖拽瀏覽器視窗大小,視窗列印是1秒鐘進行了10次請求,而我們只需要2,3次。我們可以按照時間段來忽略掉一些事件請求。例如確保500ms只列印一次。很顯然,我們可以通過setTimeout來完成。 函式節流實現: 實現函式節流的程式碼有許多種,下面實現的原理是:將即將被執行的函式用setTimeout延時,如果該次延遲執行的還沒有完成,那麼忽略接下來呼叫該函式的請求。函式接收2個引數:第一個是需要執行的被延遲的函式,第二個引數是延遲的具體時間。
var throttle = function (fn, interval){
var _self = fn; // 保留需要被延遲執行的函式的引用
var timer = null; // 定時器
var firstTime = true; // 是否是第一次呼叫
return function() {
var args = arguments;
var _me = this;
if (firstTime) { // 如果是第一次呼叫,不需要延遲
fn.apply(_me, args);
return firstTime = false;
}
if(timer) { // 如果timer還沒有執行,說明上一次的延遲執行還沒完成,本次的呼叫去掉
return false;
}
timer = setTimeout(function() { // 延遲一段時間執行
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 500);
}
}
window.onresize = throttle(function() {
console.log(1);
}, 500);
複製程式碼
5.4 分時函式
某些函式確實是由使用者主動呼叫,但是因為一些客觀原因,這些函式會引起嚴重的效能問題。例如QQ的好友列表,有1000個好友。那麼我們需要建立1000個好友節點並插入到頁面中。那麼短時間一次需要渲染插入1000個節,點,會讓瀏覽器吃不消,就會出現瀏覽器的卡頓甚至假死。
var arr = []; // 創造1000個好友假資料
for (var i = 0; i < 1000; i++) {
arr.push(i);
}
// 建立節點
var renderList = function(data) {
var currentTime = (new Date()).valueOf();
for (i = 0; i < data.length; i++) {
var div = document.createElement('div');
div.innerHTML = i;
document.body.appendChild(div);
}
console.log(new Date().valueOf() - currentTime);
}
renderList(arr);
複製程式碼
這個問題的解決方案就是下面的timeThunk函式。讓建立節點的工作分批進行,例如1秒建立1000個節點,修改為每個200毫秒建立8個節點。有三個引數:
- 需要建立節點的資料
- 封裝建立節點的函式
- 每一個批建立的節點數
var arr = []; // 創造1000個好友假資料
for (var i = 0; i < 1000; i++) {
arr.push(i);
}
function timeThuck(data, fn, count) {
var obj, t;
var len = data.length;
var start = function() {
for (var i = 0; i < Math.min(count || 1, data.length); i++) {
fn(data.shift());
}
}
return function() {
var currentTime = (new Date()).valueOf();
t = setInterval(function() {
if (data.length === 0) {
console.log("time", (new Date()).valueOf() - currentTime);
return clearInterval(t);
}
start();
}, 200)
}
}
function createElement(content) {
var div = document.createElement('div');
div.innerHTML = content;
document.body.appendChild(div);
}
var renderList = timeThuck(arr, createElement, 8);
renderList();
複製程式碼
5.5 惰性載入函式
由於瀏覽器間的行為差異,在JavaScript的程式碼中,為了達到不同瀏覽器的相容,經常需要寫入大量判斷語句,對於執行不同程式碼塊,比如要實現一個在諸瀏覽器間通用的事件繫結函式addHandler,可以參考如下程式碼實現:
var addHandler = function(el, type, handler) {
if ( window.addEventListener) {
return el.addEventListener(type, handler);
} else if (window.attachEvent) {
return el.attachEvent('on' + type, handler);
} else {
return el['on' + type] = handler;
}
}
複製程式碼
如上程式碼,在每次執行時都需要重新做條件判斷,我們要如何才能做到在每個環境下只做一次判斷呢?addHandler依然宣告為一個普通函式,在第一次進入條件分支時,函式內部會重寫這個函式,重寫的就是符合當前環境的addHandler函式,當再次進入addHandler函式時,函式裡不再存在條件分支語句:
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);
};
複製程式碼