[JavaScript] Javascript 所有函式都可以用的 bind(), apply(), call() 和函式借用 (Function borrowing) 與柯里化 (Function Currying)

itsems
itsems_frontend
Published in
8 min readOct 17, 2020
Photo by Alexander Andrews on Unsplash
Outline
+ bind()
+ call()
+ apply()
+ 什麼時候會用上?
a. 函式借用 (Function borrowing)
b. 函式柯里化 (Function Currying)

在前面的 [JavaScript] Javascript 的執行環境 (Execution context) 與堆疊 (Stack) 中,我們知道函式被執行 (Invoke) 的時候,JS 會建立函式執行環境,並替我們建立了變數環境、外部環境,和 this

bind(), call(), apply() 簡單說就是讓我們可以自己設定 this 要指向什麼的方法

在開始之前,先複習一個重要的觀念:

在 Javascript 裡面,所有函式都是物件

Functions are Objects.

函式在 Javascript 是一種特殊形態的物件,他跟一般的物件一樣也擁有屬性 (properties) 方法 (methods),並擁有兩種隱藏的特殊屬性,一個是名字 (name) 屬性,不一定要有名字,函式也可以是匿名的,另一個是程式 (code) 屬性,這就是你寫程式碼的地方,意思是,你寫的程式並非就是函式本身,你的程式碼只是其中程式屬性的內容,而這個屬性特別的地方,就是他是可以透過 () 被呼叫 (Invocable) 、被執行的。

為了證明函式是一種物件,你可以試著替他加上屬性:

這樣的 log 會印出函式的程式內容:

試著印出 age:

接著來找找看 bind(), call(), apply() 這三個都是函式原型本身就有的方法,

console.dir 會印出帶入對象的所有屬性和屬性值

其實你常常看到他,他會出現在這裡:

__proto__ 原型裡面打開,就看得到這三個方法了

bind()

bind() 指定 this、帶入參數,不會執行,回傳函式

  • 第一個參數:指定 this 對象
  • 第二個以後的參數:要帶入的參數
  • 回傳函式

這是一段會錯誤的程式:

按下 f12 打開 devtool 把程式貼進 console 裡一起看看結果會是什麼

我在 who 裡面呼叫了 this.getName(),但是 who 本身並沒有這個函式,這裡的 this 代表的是全域,所以會跳出找不到 getName() 的錯誤訊息:

但是我其實是想要呼叫 me 物件裡面的 getName,該怎麼做呢?

var whoAmI = who.bind(me);

建立一個新的函式,使用原本的 who func,在他的後面加上 .bind

要注意這裡的 who func 並沒有執行 (字尾沒有()),bind 是用點的方式加在後面,表示在這裡我把 who 當作物件使用,想要觸發 who 這個物件的 bind() 這個方法,並將我要指定為 this 的 me 物件帶進去。

.bind 會創建並回傳新的函式,他會複製使用他的這個函式 (who func),將指定帶入的對象 (me) 的 this 帶入這個函式 ,所以 whoAmI 就是一個 this 為 me 物件的新的函式了。

要加上 who 自己需要的參數的話,寫在後面就可以:

var whoAmI = who.bind(me, 'bind', 'method ');

改為呼叫 whoAmI() 之後:

如此就可以成功呼叫 me 裡面的 getName,並印出正確的值

或者直接在 who func 的結尾使用 bind 也可以:

var who = function (which, methods) {
console.log('I am ' + this.getName());
console.log('Logged from ' + which + ' ' + methods);
console.log('-----------');
}.bind(me, 'bind', 'method ');
who(); // 與上結果相同

call()

call() 指定 this、帶入參數,直接執行

  • 第一個參數:指定 this 對象
  • 第二個以後的參數:要帶入的參數
  • 直接執行

call 的參數引入和 bind 一樣,差別在於使用之後就會直接執行:

得到如上的結果,不需要另外再定義另一個函式。

apply()

apply() 指定 this、帶入參數陣列,直接執行

  • 第一個參數:指定 this 對象
  • 第二個以後的參數:要帶入的參數陣列
  • 直接執行

apply 跟 call 很像,差別只在帶入的參數需要是以陣列的方式

另外 call() 和 apply() 也可以像是 IIFE 的方式執行:

IIFE: Immediately Invoke Function Expressions 立即執行函式
不太熟悉的話可以參考這邊:[JavaScript] 立馬執行的立即呼叫函式 (IIFEs)

把剛剛 who func 裡面的內容拔出來,像是 IIFE 一樣用括弧包起來,但是最後面不是加上直接執行的 () ,而是加上 call() / apply(),可以達到一樣的效果

結論

總結來看,以上的範例,這三種方式各自使用會是這樣子:

  • bind():帶入 this、參數,不會執行,會回傳新的函式
  • call():帶入 this、參數,會直接執行
  • apply():帶入 this、參數陣列、會直接執行

三個方法說完了,接下來看看兩個比較常用到的例子

函式借用 (Function Borrowing)

如果我今天有第二個物件叫做 she:

我也想要用剛剛的範例 getName 取出名字的組合,但是 she 這個物件沒有 getName 函式,這個時候就可以借來用用:

用法和以上一樣,以 call 的方式來說明,就是我呼叫了 me 裡面 getName 函的 call ,並將 she 物件指定為這個函式的 this,就可以把 me 物件的函式拿來用在 she 物件上了。

函式科里化 (Function Currying)

這邊不會談太深的 Currying,有興趣的人歡迎自行谷歌一番。

科里化白話一點就是一個「將接收 n 個參數的函式拆解為一連串 n 個只接受一個參數的函式」的過程。( Currying is when you break down a function that takes multiple arguments into a series of functions that each take only one argument.)

從這樣:

變這樣:

可以如上圖一次性的執行 currying func,也可以一次帶一個參數呼叫達到類似封裝的效用:

概念上就說明到這邊,接下來我們會用 bind 來實現簡易的 currying,利用 bind 會複製出一個新的函式這個特性,加上閉包,來製作出可復用的函式:

上圖的意思是:

  • 我建了一個 add10 func
  • 用 bind 來拷貝 add func
  • 在指定 this 的地方,指定了自己 (add func),並加上 10 這個參數

指定了自己,並加上 10 的這個參數,將成為 add10 func 的永久參數值,a 將永遠都是 10,他會鎖在 add10 func 裡面,每次要調用的時候,其實會像是這樣:

a 一定是 10,所以任何要 +10 的數字,我只要調用 add10 func,填入要加的數字就可以了:

這樣就可以自己延伸製作需要的函式,不用重新再寫一個:

另外剛剛說的永久參數值,表示是無法變動的,如果我在 bind 的時候把兩個參數都帶入了:

結果就會是 12 而不是 15。

內容若有任何錯誤,歡迎留言交流指教!🐖

ref:
JavaScript 全攻略:克服 JS 的奇怪部分
What is ‘Currying’?
wikipedia-柯里化

--

--

itsems
itsems_frontend

Stay Close to Anything that Makes You Glad You are Alive.