征服 JavaScript 面試:什么是函數組合

SH101460 7年前發布 | 9K 次閱讀 JavaScript開發 JavaScript

Google 數據中心管道 — Jorge Jorquera — (CC-BY-NC-ND-2.0)

“征服 JavaScript 面試”是我寫的一系列文章,來幫助面試者準備他們在面試 JavaScript 中、高級職位中將可能會遇到的一些問題。這些問題我自己在面試中也經常會問。

函數式編程正在接管 JavaScript 世界。就在幾年前,只有少數 JavaScript 程序員知道函數式編程是什么。然而,在過去 3 年內,我所看到的每個大型應用程序代碼庫都大量用到了函數式編程理念。

函數組合就是組合兩到多個函數來生成一個新函數的過程。將函數組合在一起,就像將一連串管道扣合在一起,讓數據流過一樣。

簡而言之,函數 f 和 g 的組合可以被定義為 f(g(x)) ,從內到外(從右到左)求值。也就是說,求值順序是:

x
g
f

下面我們在代碼中更近距離觀察一下這個概念。假如你想把用戶的全名轉換為 URL Slug,給每個用戶一個個人信息頁面。為了實現此需求,你需要經歷一連串的步驟:

  1. 將姓名根據空格分拆(split)到一個數組中

  2. 將姓名映射(map)為小寫

  3. 用破折號連接(join)

  4. 編碼 URI 組件

如下是一個簡單的實現:

const toSlug = input => encodeURIComponent(
  input.split(' ')
    .map(str => str.toLowerCase())
    .join('-')
);

還不賴...但是假如我告訴你可讀性還可以更強一點會怎么樣呢?

假設每個操作都有一個對應的可組合的函數。上述代碼就可以被寫為:

const toSlug = input => encodeURIComponent(
  join('-')(
    map(toLowerCase)(
      split(' ')(
        input
      )
    )
  )
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

這看起來比我們的第一次嘗試更難讀懂,但是先忍一下,我們就要解決。

為了實現上述代碼,我們將組合幾種常用的工具,比如 split() 、 join() 和 map() 。如下是實現:

const curry = fn => (...args) => fn.bind(null, ...args);

const map = curry((fn, arr) => arr.map(fn));

const join = curry((str, arr) => arr.join(str));

const toLowerCase = str => str.toLowerCase();

const split = curry((splitOn, str) => str.split(splitOn));

除了 toLowerCase() 外,所有這些函數經產品測試的版本都可以從 Lodash/fp 中得到。可以像這樣導入它們:

import { curry, map, join, split } from 'lodash/fp';

也可以像這樣導入:

const curry = require('lodash/fp/curry');
const map = require('lodash/fp/map');
//...

這里我偷了點懶。注意這個 curry 從技術上來說,并不是一個真正的柯里化函數。真正的柯里化函數總會生成一個一元函數。這里的 curry 只是一個偏函數(partial application)。不過,這里只是為了演示用途,我們就把它當作一個真正的柯里化函數好了。

回到我們的 toSlug() 實現,這里有一些東西真的讓我很煩:

const toSlug = input => encodeURIComponent(
  join('-')(
    map(toLowerCase)(
      split(' ')(
        input
      )
    )
  )
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

對我來說,這里的嵌套太多了,讀起來有點讓人摸不著頭腦。我們可以用一個會自動組合這些函數的函數來扁平化嵌套,就是說,這個函數會從一個函數得到輸出,并自動將它傳遞給下一個函數作為輸入,直到得到最終值為止。

細想一下,好像數組中有一個函數可以做差不多的事情。這個函數就是 reduce() ,它用一系列值為參數,對每個值應用一個函數,最后累加成一個結果。值本身也可以函數。但是 reduce() 是從左到右遞減,為了匹配上面的組合行為,我們需要它從右到左縮減。

好事情是剛好數組也有一個 reduceRight() 方法可以干這事:

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

像 .reduce() 一樣,數組的 .reduceRight() 方法帶有一個 reducer 函數和一個初始值( x )為參數。我們可以用它從右到左迭代數組,將函數依次應用到每個數組元素上,最后得到累加值( v )。

用 compose ,我們就可以不需要嵌套來重寫上面的組合:

const toSlug = compose(
  encodeURIComponent,
  join('-'),
  map(toLowerCase),
  split(' ')
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

當然,lodash/fp 也提供了 compose() :

import { compose } from 'lodash/fp';

或者:

const compose = require('lodash/fp/compose');

當以數學形式的組合從內到外的角度來思考時,compose 是不錯的。不過,如果想以從左到右的順序的角度來思考,又該怎么辦呢?

還有另外一種形式,通常稱為 pipe() 。Lodash 稱之為 flow() :

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'

const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!

可以看到,這個實現與 compose() 幾乎完全一樣。唯一的不同之處是,這里是用 .reduce() ,而不是 .reduceRight() ,即是從左到右縮減,而不是從右到左。

下面我們來看看用 pipe() 實現的 toSlug() 函數:

const toSlug = pipe(
  split(' '),
  map(toLowerCase),
  join('-'),
  encodeURIComponent
);

console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'

對于我來說,這要更容易讀懂一些。

骨灰級的函數式程序員用函數組合定義他們的整個應用程序。而我經常用它來消除臨時變量。仔細看看 pipe() 版本的 toSlug() ,你會發現一些特殊之處。

在命令式編程中,在一些變量上執行轉換時,在轉換的每個步驟中都會找到對變量的引用。而上面的 pipe() 實現是用 無點 的風格寫的,就是說完全找不到它要操作的參數。

我經常將管道(pipe)用在像單元測試和 Redux 狀態 reducer 這類事情上,用來消除中間變量。中間變量的存在只用來保存一個操作到下一個操作之間的臨時值。

這玩意開始聽起來會比較古怪,不過隨著你用它練習,會發現在函數式編程中,你是在和相當抽象、廣義的函數打交道,而在這樣的函數中,事物的名稱沒那么重要。名稱只會礙事。你會開始把變量當作是多余的樣板。

就是說,我認為無點風格可能會被用過頭。它可能會變得太密集,較難理解。但是如果你搞糊涂了,這里有一個小竅門...你可以利用 flow 來跟蹤是怎么回事:

const trace = curry((label, x) => {
  console.log(`== ${ label }:  ${ x }`);
  return x;
});

如下是你用它來跟蹤的方法:

const toSlug = pipe(
  trace('input'),
  split(' '),
  map(toLowerCase),
  trace('after map'),
  join('-'),
  encodeURIComponent
);

console.log(toSlug('JS Cheerleader'));
// '== input:  JS Cheerleader'
// '== after map:  js,cheerleader'
// 'js-cheerleader'

trace() 只是更通用的 tap() 的一種特殊形式,它可以讓你對流過管道的每個值執行一些行為。明白了么?管道(Pipe)?水龍頭(Tap)?可以像下面這樣編寫 tap() :

const tap = curry((fn, x) => {
  fn(x);
  return x;
});

現在你可以看到為嘛 trace() 只是一個特殊情況下的 tap() 了:

const trace = label => {
  return tap(x => console.log(`== ${ label }:  ${ x }`));
};

你應該開始對函數式編程是什么樣子,以及 偏函數柯里化 如何與 函數組合 協作,來幫助你編寫可讀性更強的程序有點感覺了。

 

來自:http://www.zcfy.cc/article/master-the-javascript-interview-what-is-function-composition-2160.html

 

 本文由用戶 SH101460 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!