設計模式基礎 之 4 高階函式

zhaoyezi發表於2018-05-28

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);
    };
複製程式碼

相關文章