寫JavaScript函數不得不知的高級技巧

對於咱們程序員來講,寫函數是再熟悉不過的事情了,無論咱們要實現什麼樣的功能,都須要經過函數來完成。在JavaScript裏面,函數擁有很是高的特權,甚至是一等公民,所以也跟Kotlin同樣支持多種編程範式。javascript

今天我主要想跟你們聊聊一些寫函數時的高級技巧,大概有以下幾個內容:java

  • 純函數
  • 高階函數
  • 函數緩存
  • 懶函數
  • 柯里化
  • 函數組合

純函數

純函數要知足兩個條件:程序員

  1. 給相同的參數返回相同的結果
  2. 不產生任何反作用

來看以下代碼:編程

function double(num){
  return num * 2 
}
複製代碼

這邊只要給num的值不變,它返回的結果也不會變,並且這個函數執行的過程當中沒有對外界形成影響,因此它是一個純函數。數組

而:緩存

const counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()
複製代碼

20200929120128.jpg

這個函數每次執行時結果都不同,因此不是純函數。markdown

而:app

let count = 0;
function isYoung(user){
  if(user.age <= 20){
    count++;
    return true
  }
  return false
}
複製代碼

這裏雖然每次給定相同的輸入都給出相同的結果,可是它操做了外部的變量,產生了一個反作用,因此它也不是純函數。函數式編程

純函數有什麼好處?

爲何咱們要區分純函數跟其它函數?由於純函數在咱們編碼過程當中能夠提升代碼的質量。函數

  1. 純函數更清晰更易於理解

每一個純函數都完成了一個特定的任務,而且咱們能夠經過輸入預測結果

  1. 對於純函數編譯器能夠作優化

好比說咱們有以下代碼:

for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}
複製代碼

若是fun不是純函數,那麼fun(10)將會被執行1000次,可是若是fun是一個純函數,那麼因爲對於給定的輸入它的輸出是肯定的,因此上面的代碼能夠被優化成:

const result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}
複製代碼
  1. 純函數更易於測試

純函數的測試不依賴於外部因素,多虧了純函數的特性,咱們給純函數編寫單元測試時只要簡單地給個輸入而後判斷輸出是否與預期一致就行了。

還用上面的double(num)函數爲例,咱們寫單元測試就只須要這麼寫:

const x = 1;
assert.equals(double(x),2);
複製代碼

若是不是純函數,咱們就會有許多外部的因素須要考慮,mock數據之類的。

高階函數

高階函數至少要知足下面條件中的一個:

  1. 接受函數做爲參數
  2. 把函數做爲結果返回

不瞭解函數式編程的同窗可能感受有些怪異,函數原本是計算結果的,返回另外一個函數,這有什麼用場?哎,用處可大了,使用高階函數可讓咱們的代碼變得更加簡單靈活。

咱們仍是來看個具體的例子吧,假設咱們有一個數組,咱們想用它來建立一個新的數組,這個新數組中每一個元素是以前的數組對應位置的元素+1。

不用高階函數的話,咱們大概會這麼寫:

const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] + 1);
}
複製代碼

可是JavaScript的數組對象有一個map方法,這個map方法接受一個回調,會對當前數組對象的每個元素應用這個回調,返回一個新數組。

const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item + 1;
});
console.log(arr2);
複製代碼

咱們的代碼是否是看起來更簡潔了?這個map函數就是一個高階函數,map有映射的意思,咱們掃一眼很快就能明白這段代碼聲明瞭對於原來對象的轉換,基於原來的數組對象的元素建立一個新的數組。高階函數的強大可不止這麼點,我們接着往下看。

函數緩存

假設咱們有個很耗時的純函數:

function computed(str) {    
    // 就當這裏是很耗時的計算 
    console.log('執行了10分鐘')
    // 這是計算結果
    return '算出來了'
}
複製代碼

爲了不沒必要要的重複計算,咱們能夠緩存一些以前已經計算過的結果。這樣再後面再遇到相同的計算時,咱們能夠從緩存中直接取出結果。咱們在這兒須要編寫一個名爲cached的函數去包裝咱們實際要調用的函數,這個函數把目標函數做爲參數,返回一個新的函數。在這個cached函數裏,咱們緩存以前函數調用的結果。

function cached(fn){
  // 這邊使用一個對象作緩存
  const cache = Object.create(null);

  //返回一個對目標函數加上了緩存邏輯的函數
  return function cachedFn (str) {

    //若是緩存裏沒有,咱們會執行目標函數
    if ( !cache[str] ) {
        let result = fn(str);

        //把計算結果緩存起來
        cache[str] = result;
    }

    return cache[str]
  }
}
複製代碼

咱們能夠看到以後再輸入相同的參數後咱們能夠直接拿到計算結果了。

懶函數

函數體裏面會包含各類各樣的條件語句,有時候這些條件語句僅僅須要執行一次,好比說咱們寫單例的時候判斷某個對象是否爲空,若是爲空咱們就建立一個對象,那其實咱們知道後續只要程序還在運行,這個對象是不可能爲空的,可是咱們每次使用時都還會判斷是否爲空,都會執行咱們的條件判斷。咱們能夠稍微提高一下性能經過在第一次執行後刪除這些條件判斷,這樣後面就不判斷是否爲空直接拿來即用了,這就是懶函數

咱們把上面的描述用簡單的代碼表現出來:

let instance = null;
function user() {
    if ( instance != null) {
      return instance;
    } else {
      instance = new User()
      return instance;
    }
}
複製代碼

上面的代碼在每次執行的時候都會執行條件判斷,這邊還好,若是咱們的條件判斷很是複雜,那其實也是一個不小的性能影響,這時候咱們就可使用懶函數的小技巧來優化代碼:

var user = function() {
    var instance = new User();
    user = function() {
        return instance;
    };
    return user();
}
複製代碼

這樣在第一次執行後,咱們用一個新函數重寫了以前的函數,後面再執行這個函數的時候咱們都會直接返回一個固定的值,這無疑會提升咱們代碼的性能。因此後續咱們遇到一些只用執行一次的條件語句,咱們均可以用懶函數來優化它,經過使用一個新函數來覆蓋原有的函數來移除條件語句。

函數柯里化

柯里化簡單來講就是把一個接受多個參數的函數轉化成一串接受單個參數的函數,這麼說可能有點繞,其實就是把一個一次性接受一堆參數的函數,轉化成接受第一個參數返回一個接受第二個參數的函數,這個函數返回一個接受第三個參數返回一個接受第四個參數的函數,以此類推。

可能好多同窗第一次遇到不知道它有什麼用,能一次調用完爲何要整這麼花裏胡哨呢?

  1. 柯里化可讓咱們避免重複傳相同的值
  2. 這其實上是建立了一個高階函數,方便咱們處理數據

咱們來看一個簡單的求和的函數,它接受三個數字做爲參數並返回它們的和。

function sum(a,b,c){
 return a + b + c;
}
複製代碼

多幾個少幾個參數均可以成功調用它:

sum(1,2,3) --> 6 
sum(1,2) --> NaN
sum(1,2,3,4) --> 6 //多餘的參數被忽略了
複製代碼

那麼怎樣咱們才能把它轉化成一個柯里化的版本呢?

function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (...args) => {
        if (fn.length === args.length) {

            return fn(...args)
        } else {
            return (...args2) => {

                return generator(...args, ...args2)
            }
        }
    }
    return generator
}
複製代碼

看個例子:

咱們能夠得到跟以前一梭子傳遞全部參數同樣的結果,同時咱們還能夠在任何一步中緩存以前計算的結果,好比咱們此次要傳入(1,2,3,6),那咱們是能夠避免對前面三個參數進行重複計算的。

函數組合

假設咱們須要實現一個把給定數字乘10而後轉成字符串輸出的功能,那咱們須要作的有兩件事:

  • 給定數字乘10
  • 數字轉字符串

咱們拿到手大概會這麼寫:

const multi10 = function(x) { return x * 10; };
const toStr = function(x) { return `${x}`; };
const compute = function(x){
    return toStr(multi10(x));
};
複製代碼

這邊只有兩步,因此看起來不復雜,實際狀況是若是有更多的操做的話,層層嵌套很難看也容易出錯,相似於這樣fn3(fn2(fn1(fn0(x))))。爲了不這種狀況,把調用層級扁平化,咱們能夠寫一個compose函數專門用來把函數調用組合到一塊兒:

const compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};
複製代碼

以後咱們的compute函數就能夠這麼寫了:

let `compute` = compose(toStr, multi10);
compute(8);
複製代碼

經過使用compose函數咱們能夠把兩個函數組合成一個函數,這讓代碼從右往左執行,而不是層層計算某個函數的結果做爲另外一個函數的參數,這樣代碼也更加直觀。可是如今compose僅僅支持兩個參數,不要緊咱們能夠寫一個支持任意參數的版本:

function compose(...funs){
    return (x)=>funs.reduce((acc, fun) => fun(acc), x)
}
複製代碼

如今咱們的compose函數對於參數個數再也不有限制了:

經過函數組合,咱們能夠能夠聲明式地指定函數間的關係,代碼的可讀性也大大提升,也方便咱們後續對代碼進行擴展跟重構,並且在React裏面,當咱們的高階組件變多的時候,一個套着一個就很難看,咱們就能夠經過相似的方式來讓咱們的高階組件層級扁平化。

好啦,今天的分享就到這裏啦,咱們能夠看到仍是有不少咱們能夠玩轉的技巧的,把這些技巧運用起來,讓咱們的代碼更加優雅吧~ happy coding~

相關文章
相關標籤/搜索