JavaScript函數(shù)編程中的一些值得學(xué)習(xí)的技術(shù)

純函數(shù), 高階函數(shù),函數(shù)組合,函數(shù)柯里化,偏函數(shù),惰性載入函數(shù),緩存函數(shù)
這些概念在函數(shù)編程中真的是太常見了,尤其是很多類庫實(shí)現(xiàn)或者組件封裝都會(huì)用到這些函數(shù)編程技巧。

比如React-redux中的connect方法,React中的高階組件其實(shí)都或多或少用到了上述一些函數(shù)編程技巧。剛好最近有幸看到一篇關(guān)于這方面的文章,記錄一下。

在JS中,函數(shù)總是被稱為一等公民,那到底為什么會(huì)被稱為一等公民呢?主要是因?yàn)?,在JS中函數(shù)可以作為普通變量一樣使用,可以作為函數(shù)的參數(shù),可以被賦值,可以作為函數(shù)的return值。這樣就導(dǎo)致了函數(shù)在JS中具有極其靈活的用法,因此也有了更加強(qiáng)大的功能。

那下面就介紹下上面所說的這些內(nèi)容,在閱讀別人代碼或者自己封裝一些方法時(shí)都會(huì)很有用,最重要的是面試的時(shí)候被面試官問到了,不至于完全不知道而尷尷尬尬。

1. 純函數(shù)
一個(gè)函數(shù)的返回結(jié)果只依賴于它的參數(shù),并且在執(zhí)行過程里面沒有副作用,我們就把這個(gè)函數(shù)叫做純函數(shù)

重點(diǎn)有兩點(diǎn):一是返回結(jié)果只依賴它的參數(shù),二是執(zhí)行過程中沒有副作用。

執(zhí)行過程沒有副作用是指,不會(huì)對函數(shù)外面的變量造成任何影響。

let num = 0;

// bar不是純函數(shù),因?yàn)榉祷刂狄蕾嚵送獠孔兞縩um
function bar(a, b) {
    return a+b+num;
}

// laa不是純函數(shù),執(zhí)行過程中產(chǎn)生副作用,影響外面num變量
function laa(a, b) {
    num++;
    return a+b;
}

// foo是純函數(shù)
function foo(a, b) {
    return a+b;
}

看一下上面的例子就ok了。使用純函數(shù)的原因是因?yàn)樗奶攸c(diǎn)是比較靠譜,接收相同的參數(shù),就一定能輸出相同的值,這樣的程序易于調(diào)試,不易出現(xiàn)莫名其妙的問題。

2. 高階函數(shù)
高階函數(shù)有兩種形式:其一是函數(shù)的參數(shù)是另一個(gè)函數(shù)(回調(diào)函數(shù))其二是函數(shù)的返回值是一個(gè)函數(shù)。

函數(shù)參數(shù)為一個(gè)函數(shù),可以理解成回調(diào)函數(shù)就是高階函數(shù)的一種,這樣理解我覺得沒有什么錯(cuò)??梢詤⒖贾?a target="_blank">回調(diào)函數(shù)和高階函數(shù)的區(qū)別?
提問下的回答。

ES6版本常見的數(shù)組的方法們map filter reduce等等都是高階函數(shù)。

函數(shù)的返回值是一個(gè)函數(shù),舉個(gè)例子,比如防抖(debouce)函數(shù)和節(jié)流(throttle)函數(shù)
借助下lodash的例子看下

// 避免窗口在變動(dòng)時(shí)出現(xiàn)昂貴的計(jì)算開銷。
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 
// 當(dāng)點(diǎn)擊時(shí) `sendMail` 隨后就被調(diào)用。
jQuery(element).on('click', _.debounce(sendMail, 300, {
  'leading': true,
  'trailing': false
}));

接收一個(gè)或多個(gè)函數(shù)作為輸入,經(jīng)過加工最終輸出一個(gè)新的函數(shù),中間可以定義一些其他邏輯。比如再看個(gè)小例子:

function Eat(a,b){ //核心業(yè)務(wù)代碼
  console.log(a,b)
}

Function.prototype.before = function(callback){     //高階函數(shù)
  return (...args)=>{ //使用rest運(yùn)算符接收
    callback();
    this(...args);  //使用展開運(yùn)算符傳入
  }
}

let beforeEat = Eat.before(function(){ //自己擴(kuò)展業(yè)務(wù)代碼
  console.log("before eat")
})
beforeEat("米飯","牛肉") //傳參

可以用來擴(kuò)展函數(shù)功能。

3. 函數(shù)組合

函數(shù)組合就是將功能單一的函數(shù),進(jìn)行組合返回一個(gè)功能更加強(qiáng)大的函數(shù)。(如果看過鎧甲勇士的話,想象一下金木水火土合成帝皇俠那種感覺,如果沒看過就算了)

為了降低耦合性,我們封裝的函數(shù)功能性比較單一,便于在不同的場景下使用。比如如下函數(shù):

function lowerCase(str) {
   return str.toLowerCase();
}

function upperCase(str){
  return str.toUpperCase();
}

function trim(str) {
  return str.trim();
}

三個(gè)函數(shù)分別代表三個(gè)功能,當(dāng)有一天忽然需要轉(zhuǎn)換小寫和去除字符串的頭尾空格的函數(shù),比如lowerCaseAndtrim方法這時(shí)候可以實(shí)現(xiàn)一個(gè)compose方法如下調(diào)用。

let lowerCaseAndtrim = compost(lowerCase, trim);
lowerCaseAndtrim('  JaVascRipt  '); 

最終目標(biāo)輸出 javascript;

接下來實(shí)現(xiàn)一下compose函數(shù)
思路:compose函數(shù)返回值是一個(gè)函數(shù)

function compose(...funcs) {
    return function(x) {
        let result = x;
        for(let i = 0; i < funcs.length; i++) {
            result = funcs[i](result)
        }
        return result;
    }
}

遍歷執(zhí)行compose方法接收的每一個(gè)方法,將目標(biāo)參數(shù)逐一交給每個(gè)函數(shù)執(zhí)行最終返回。

借助 Array.prototype.reduce實(shí)現(xiàn)

function compose(...funcs) {
    return (x) => {
        return funcs.reduce((prev, next) => {
            return next(prev);
        }, x)
    }
}

4. 函數(shù)柯里化
函數(shù)柯里化是一種可以實(shí)現(xiàn)函數(shù)多參變單參的方式,在柯里化的過程總,將一個(gè)帶有多個(gè)參數(shù)的函數(shù),轉(zhuǎn)換為帶有一個(gè)參數(shù)的一系列的嵌套函數(shù)。它返回一個(gè)新函數(shù),這個(gè)新函數(shù)期望傳入下一個(gè)參數(shù)。當(dāng)接收足夠的參數(shù)后,會(huì)自動(dòng)執(zhí)行原函數(shù)

function add(a, b, c) {
  return a+b+c;
}
let addCurry = curry(add) // curry就是函數(shù)實(shí)現(xiàn)柯里化的一個(gè)方法
addCurry(1)(2)(3); // 本來應(yīng)該add(1,2,3)調(diào)用的 進(jìn)行柯里化之后就可以addCurry(1)(2)(3) 調(diào)用了

關(guān)于函數(shù)柯里化的討論可以參考柯里化對函數(shù)式編程有何意義?

柯里化實(shí)現(xiàn):
思路:返回值是一個(gè)函數(shù),需要根據(jù)原函數(shù)參數(shù)是否等于調(diào)用時(shí)傳入的參數(shù)個(gè)數(shù)來判斷是否相等,相等直接傳入?yún)?shù)執(zhí)行原函數(shù),否則返回函數(shù),繼續(xù)接收參數(shù),并遞歸判斷原函數(shù)參數(shù)是否等于調(diào)用時(shí)傳入的參數(shù)個(gè)數(shù)邏輯。

function _curry(func) {
    return function curried(...args) {
        if(func.length === args.length) {
            return func.apply(this, args)
        } else {
            return function(...args2) {
                return curried.apply(this, args.concat(args2));
            }
        }
    }
}

5. 偏函數(shù)

偏函數(shù)指的是固定函數(shù)某些參數(shù),從而產(chǎn)生更小元的函數(shù)。元指的是函數(shù)的參數(shù)個(gè)數(shù)。
比如封裝函數(shù)的時(shí)候函數(shù)一共有三個(gè)參數(shù),第一個(gè)參數(shù)是固定不變的,后面的參數(shù)是變化的那么就可以使用偏函數(shù)。當(dāng)然也可以固定前兩個(gè)參數(shù)。

function add(int, a, b){
  return int+a+b;
}
let partialAdd = partial(add, 10);
partialAdd(1,2);  // 輸出13
partialAdd(3,4); // 輸出17

上面是偏函數(shù)使用形式,現(xiàn)在實(shí)現(xiàn)一個(gè)偏函數(shù)

function partial(func, ...args){
  return (...args2) => {
    return func.apply(this, [...args, ...args2]);
  }
}

6. 惰性載入函數(shù)
惰性載入函數(shù)旨在提升代碼性能。比如函數(shù)需要根據(jù)不同的判斷結(jié)果返回不同的值,每一次執(zhí)行函數(shù)的時(shí)候就需要判斷一次,判斷的邏輯如果復(fù)雜的話,那么對于性能消耗就比較大。尤其是當(dāng)判斷條件是固定的時(shí)候使用惰性載入函數(shù)是非常適合的。

比如JS代碼判斷運(yùn)行平臺是安卓還是IOS

let isIos = 1;
function engin() {
    if(isIos){
        console.log('ios');
        // ios邏輯
    } else {
        console.log('android');
        // 安卓邏輯
    }
}
engin();
engin();

執(zhí)行這個(gè)engin方法的時(shí)候次都要去走判斷平臺邏輯,性能肯定是消耗滴,我們要的是當(dāng)?shù)谝淮螆?zhí)行的時(shí)候判斷下平臺邏輯,之后每一次執(zhí)行就不需要去判斷了,因?yàn)槠脚_是不變的,可以節(jié)省一些開銷。

惰性載入函數(shù)的實(shí)現(xiàn)1:利用命名覆蓋

 const isIos = 1;

let engin = function() {
    console.log('這里只執(zhí)行一次哦'); // 注意這里
    if(isIos){
        engin = function() {
            console.log('ios');
             // ios邏輯
        }
    } else {
        engin = function() {
            console.log('android');
            // 安卓邏輯
        }
    }
    return engin();
}
engin(); // ios
engin(); // ios
engin(); // ios

最終ios輸出3次,但是上面的console只輸出一次。

惰性載入函數(shù)的實(shí)現(xiàn)2:利用立即執(zhí)行函數(shù)

const isIos = 1;
let engin = (function() {
    if(isIos){
        return function() {
            console.log('ios');
             // ios邏輯
        }
    } else {
        return function() {
            console.log('android');
            // 安卓邏輯
        }
    }
})();
engin();
engin();

JS文件加載時(shí)直接執(zhí)行一次,之后每次調(diào)用都是執(zhí)行判斷之后的邏輯。對于性能優(yōu)化還是比較有用的。

7. 緩存函數(shù)
緩存函數(shù),指的是根據(jù)函數(shù)參數(shù)將函數(shù)執(zhí)行的結(jié)果緩存起來,當(dāng)下次再調(diào)用的時(shí)候不用去計(jì)算就可以直接拿到結(jié)果,很明顯,這是一個(gè)空間換時(shí)間的做法。
直接寫實(shí)現(xiàn)了。

function memorize(func) {
  let cache = Object.create(null);
  return (...args) => {
    let key = JSON.stringify(args);
    return cache[key] || (cache[key] = fn.apply(this, args));
  }
}
function add(a,b) {
    console.log('lala');
    return a+b;
}

let d = memorize(add);
console.log(d(1,2)); // 計(jì)算獲取
console.log(d(1,2)); // 從緩存獲取
console.log(d(1,3)); // 計(jì)算獲取
console.log(d(1,3)); // 從緩存獲取

當(dāng)?shù)诙我韵嗤瑓?shù)去計(jì)算的時(shí)候,默認(rèn)首先從緩存中找,如果找到對應(yīng)的值,那么直接返回,不參與計(jì)算。這樣程序運(yùn)行速度會(huì)比較快,但是,比較消耗內(nèi)存。

以上就是一些關(guān)于函數(shù)編程中的一些技巧使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容