[JavaScript] Javascript 所有函式都可以用的 bind(), apply(), call() 和函式借用 (Function borrowing) 與柯里化 (Function Currying)
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() 這三個都是函式原型本身就有的方法,
其實你常常看到他,他會出現在這裡:
在 __proto__
原型裡面打開,就看得到這三個方法了
bind()
bind() 指定 this、帶入參數,不會執行,回傳函式
- 第一個參數:指定 this 對象
- 第二個以後的參數:要帶入的參數
- 回傳函式
這是一段會錯誤的程式:
我在 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-柯里化