點燈坊

失くすものさえない今が強くなるチャンスよ

ECMAScript 之 Generic Method

Sam Xiao's Avatar 2019-07-27

ECMAScript 有個很有趣特性:Generic Method 可被其他型別透過 call() 使用,宛如自己的 Method 一般,這種 借用 Method 特性,使得該型別也擁有其他型別能力。

Version

macOS Mojave 10.14.5
VS Code 1.36.1
Quokka 1.0.238
ECMAScript 5
Ramda 0.26.1

Function.prototype.call()

let data = 'FP in JavaScript';

let usrFn = str => [].map.call(str, x => String.fromCharCode(x.charCodeAt() + 1)).join('');

usrFn(data); // ?

data 為 string,我們希望將每個 char 的 ASCII code 加 1 後再轉為 string。

若將 string 看成 array,這是很簡單的 map() 而已,但可惜 string 沒有 map() 可用。

因為 Array.prototype.map() 為 generic method,可透過 call() 被 string 借用,宛如 String.prototype.map() 一般。

至於 ASCII code 加 1,可透過 fromCharCode()charCodeAt() 實現。

最後再使用 join('') 將 array 轉回 string。

generic000

根據 ECMAScript spec,以下 method 均為 generic method,可使用 call() 被其他型別借用,因此稱為 generic

  • Array.prototype
    • concat()
    • every()
    • filter()
    • forEach()
    • indexOf()
    • join()
    • lastIndexOf()
    • map()
    • pop()
    • push()
    • reduce()
    • reduceRight()
    • reverse()
    • shift()
    • slice()
    • some()
    • sort()
    • splice()
    • toLocaleString()
    • toString()
    • unshift()
  • Date.prototype
    • toJSON()
  • Object.prototype
  • String.prototype
    • charAt()
    • charCodeAt()
    • concat()
    • indexOf()
    • lastIndexOf()
    • localeCompare()
    • match()
    • replace()
    • search()
    • slice()
    • split()
    • substring()
    • toLocaleLowerCase()
    • toLocaleUpperCase()
    • toLowerCase()
    • toUpperCase()
    • trim()

Functional

import { pipe, map, join, add } from 'ramda';

let data = 'FP in JavaScript';

// toAscii :: String -> Number
let toAscii = str => str.charCodeAt();

// fromAscii :: Number -> String
let fromAscii = String.fromCharCode;

// mapFn :: String -> String
let mapFn = pipe(
  toAscii,
  add(1),
  fromAscii
);

// usrFn :: String -> String
let usrFn = pipe(
  map(mapFn),
  join('')
);

usrFn(data); // ?

String 雖然如願以償得到了 map(),但會發現 usrFn() 可讀性並不高,code 風格呈橫向發展。

原因在於 OOP 將 data 與 function 合一,function 必須掛在 data 下,而不能使用 pure function 與 pipeline 解決問題。

18 行

// usrFn :: String -> String
let usrFn = pipe(
  map(mapFn),
  join('')
);

其實 usrFn() 的大架構就是先用 map() 處理,最後再使用 join() 轉回 array,至於 map() 細節則由 mapFn() 處理。

11 行

// mapFn :: String -> String
let mapFn = pipe(
  toAscii,
  add(1),
  fromAscii
);

mapFn() 流程為:

  • toAscii() 負責將 char 轉 ascii 值
  • add(1) 負責將 ascii 值加 1
  • fromAscii() 負責將 ascii 轉回 char

最後用 pipe() 整合所有 function,非常清楚。

第 5 行

// toAscii :: String -> Number
let toAscii = str => str.charCodeAt();

// fromAscii :: Number -> String
let fromAscii = String.fromCharCode;

charCodeAt()fromCharCode() 重新以 pure function 形式改寫,適合 FP 使用 pipeline。

可以發現 FP 會不斷建立自訂 function,但卻看不到自訂 variable,因為 FP 會以 pipeline 處理,中繼 variable 都不見了

generic001

Conclusion

  • 借用 method 是 ECMAScript 獨門概念,藉由 call() 可傳入 thisArg 取代 this,使得 generic method 可被其他 object 使用
  • OOP 強調 data 與 function 合一,因此 function 會以 method 形式掛在 data 上,若該型別 data 想使用其他型別的 method,且該 method 已經被認定是 generic method,就可使用 call()借用 method,如同自己的 method 一般
  • FP 並不存在 借用 method 概念,因為 FP 是 data 與 function 分離,function 並不是掛在 data 上的 method,只要 function 接受該型別 data 就可使用,比 OOP 彈性且更容易理解

Reference

Dr. Axel Rauschdayer, Array-Like Objects and Generic Methods