1. 程式人生 > 實用技巧 >JS 高階函式

JS 高階函式

Function Object

什麼是函式?在大多數程式語言中,函式是一段獨立的程式碼塊,用來抽象處理某些通用功能的方法;主要操作是給函式傳入特定物件(引數),並在方法呼叫結束後獲得一個新的物件(返回值)。

function greeting(name) {
  return `Hello ${name}`;
}

console.log( greeting('Onion') ); // Hello Onion

但是在JavaScript、Haskell、Clojure 這類語言中,函式是另一種更高階的存在,俗稱一等公民;它除了是程式碼塊以外,它還是一種特殊型別的物件——Function Object。

為什麼說 Fuction 也是物件呢?還是看上面的示例函式——greeting,我們事實上是可以打印出它的固有屬性(properties)的:

console.log(greeting.length, greeting.name);  // 1 'greeting'

這裡length是引數列表長度,name就是它定義的名字了。是不是和物件很接近了?我們甚至可以給它新增新的屬性和方法:

greeting.displayName = 'Garlic';
greeting.innerName = () => 'Ginger';

console.log(greeting.displayName); // Garlic
console.log(greeting.innerName()); // Ginger

是吧?這麼看,函式已經包含了幾乎所有的 Object 功能了。當然,生產中儘量不要給函式新增隨機屬性,畢竟程式碼是給人閱讀的,不要隨便增加團隊的認知成本。

high order function

上面提到了函式是一種特殊的物件,因此在js語言中,函式事實上也可以像普通 object 一樣成為其他函式裡的引數或是返回值。我們將引數或是範圍值為函式的函式稱為高階函式

Higher-Order function is a function that receives a function as an argument or returns the function as output

Function 引數

我們先看一下函式如何成為引數,最經典的案例就是 Array#map。給了例子,實現一個讓陣列所有元素+1 的操作,傳統的做法如下所示:

const arr1 = [1, 2, 3];
const arr2 = [];

for(let i = 0; i < arr1.length; i++) {
  arr2.push(++arr1[i]);
}
console.log(arr2)

如果使用高階函式 map:

const arr1 = [1, 2, 3];

const arr3 = arr1.map( function callback(element, index, array) {
  return element+1;
});

console.log(arr3); // [2, 3, 4]

map 是 Array.prototype 的原生方法,它的第一個引數是一個 callback 函式,第二個引數是用來繫結 callback 的 this。這裡,callback 的作用是迭代呼叫數組裡的元素,並將返回值組裝成一個新的陣列。這個 map 的函式引數本身還有三個引數:element,index 和 array,分別表示迭代時的元素,索引,以及原始陣列。

上面的程式碼使用 es6 的箭頭函式,可以寫得更簡潔一點:

const arr1 = [1, 2, 3];

const arr3 = arr1.map(e => e+1);

console.log(arr3); // [2, 3, 4]

講真,我們經常用到高階函式,Array 裡還有好多類似的函式,如 fliter、reduce 等等。這類高階函式可以明顯的改善程式碼質量,並切能確保不會對原始陣列產生副作用。

Fucntion 返回值

返回值是函式的函式,我們也經常使用,最著名的就是 Function#bind。

給個案例,如下函式 greeting 會打印出this的name,但是 greeting 並不是一個純函式,因為它的 this 繫結不明確,可能會在不同的執行上下文中會返回不同的結果。

function greeting() {
  return `Hello ${this.name}`;
}

如果想明確它的結果該怎麼辦呢?嗯,為 greeting 繫結一個 object。這個 helloOnion 就是greeting.bind後返回的新函式。

let helloOnoin = greeting.bind({name: 'Onion'});

console.log(helloOnoin()); // Hello Onion

bind方法建立一個新的函式,在bind被呼叫時,這個新函式的this被bind的第一個引數指定,其餘的引數將作為新函式的引數供呼叫時使用。我們可以試著寫一個乞丐版的 myBind 方法(bind 還能繫結引數,這個先略過了),這樣可以更清晰地看到什麼是返回函式的高階函數了。

Function.prototype.myBind = function(context) {
  let func = this; // method is attached to the prototype, so just refer to it as this.
  return function newFn() {
    return func.apply(context, arguments);
  }
}

這裡給 Function 的原型鏈加了一個新的函式 myBind,並用到了閉包(在記憶體裡保留了原始函式和目標this);之後,呼叫 myBind 返回一個新的函式,並且在該函式執行時呼叫原始函式,左後通過apply執行時繫結目標 this。看一下效果:

let helloOnoin = greeting.myBind({name: 'Onion'});

console.log(helloOnoin()); // Hello Onoin

我這裡再寫一個健壯一點的 bind 實現,大家自己體會一下,bind 是如何將前幾個引數也綁定了的:

Function.prototype.bind = function(context, ...args) {
  let func = this;
  return function () {
    return func.call(context, ...args, ...arguments);
  }
}

函式柯里化

高階函式還在一種叫柯里化的方法裡大顯身手。

在數學和電腦科學中,柯里化是一種將使用多個引數的函式轉換成一系列使用一個引數的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。

柯里化,通俗點說就是先給原始函式傳入幾個引數,它會生成一個新的函式,然後讓新的函式去處理接下來的引數。我們先不去管 curry 的實現,看看柯里函式的用法。比如,實現一個 add 函式——簡單的兩數相加,正常的執行就是直接加兩引數執行——add(1,2)。但是這裡我們先給它做個柯里化處理,併產生了一個新的函式——curryingAdd。

function curry(fn) { ... }

function add(a, b) { return a+b; }

const curryingAdd = curry(add);

柯里化後的 curryingAdd,從普通函式變成了高階函式:它支援一次傳入一個引數(比如 10)並返回一個新的函式——addTen。我們執行addTen(1),它會記錄之前已經傳入的 10,並把 10 和 1 相加得到 11。是不是覺得很沒用?哈哈,這說明你 FP 學的不夠深。

const addTen = curryAdd(10);

console.log(addTen(1)); // 11
console.log(addTen(100)); // 110

柯里化的作用就是將普通函式轉變成高階函式,實現動態建立函式、延遲計算、引數複用等等作用。篇幅有限,我不做深入講解了。它實現上,就是返回一個高階函式,通過閉包把傳入的引數儲存起來。當傳入的引數數量不足時,遞迴呼叫 bind 方法;數量足夠時則立即執行函式。學習一下JavaScript的高階用法還是有意義的。

function curry(fn) {
  const arity = fn.length;

  return function $curry(...args) {
    if( args.length < arity ) {
      return $curry.bind(null, ...args);
    }
    return fn.apply(null, args);
  }
}

廣州品牌設計公司https://www.houdianzi.com

compose

compose 也是一個高階函式裡重要的一課。compose 就是組合函式,將子函式串聯起來執行,一個函式的輸出結果是另一個函式的輸入引數,一旦第一個函式開始執行,會像多米諾骨牌一樣推導執行後續函式。還是舉個例子:我實現了一個帶 Hello 的greeting函式,並希望在greeting呼叫結束後把返回值都顯示成大寫狀態。

const greeting = name => `Hello ${name}`;
const toUpper = str => str.toUpperCase();

toUpper(greeting('Onion')); // HELLO ONION

傳統的手段就是巢狀兩個函式使用——toUpper(greeting('Onion')),但是有時候這種巢狀可能會很多,比如下面這個態勢:

f(g(h(i(j(k('Onion'))))))

再看看 compose 的用法:

const composedFn = compose(f, g, h, i, j, k)
console.log( composedFn('Onion') )

是不是這一個 composedFn 函式比那種一層層的巢狀要美觀的多?OK,怎麼實現 compose 函式呢?把原始碼貼在這裡了。如果你覺得寫(...fns) => (...args) => ..這類程式碼不可思議的話,建議吭一下上面提到的教程《Mostly adequate guide》,吭完你就發現再正常不過了。

// compose: ( (a->b), (b->c), ..., (y->z) ) -> a -> z
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.apply(null, res)], args)[0];