[JavaScript] 函數原型最實用的 3 個方法 — call、apply、bind
Function.prototype裡頭提供了三個非常相似且常常讓人感到迷茫的 3個方法,分別是 call 、 apply 、 bind ,這三個功能可以改變函數的動態 this ,我們在這篇文章中不會·過·多·討·論· this ,只討論其功能與他們的前世今生。
這篇文章針對a/b/c三者做比較,會在後頭代入一些 this 的概念,最後以一個實務的例子比較常見的 self 、 bind 與 arrow function 解決問題的方法。
好,那我們就開始吧!
為什麼我們需要 apply / bind / call ?
答: JavaScript (或是有些人喜歡稱呼其 ECMAScript ),本身是一門動態語言,而這個動態聽起來是非常的抽象,你可能會覺得「我也寫過很多直譯語言是動態的啊」、「我知道,不就有一些動態執行的方法而已嗎?」,但我想說的是,會需要這三者,最大的原因還是因為 this 這個東西是動態的。( PS. arguments 也是)
我們直接來破題這三者最常見的用法:
我們可以發現這三者都擁有一樣的能力去完成同一件事,這邊直接討論三者的用法以及差異,乃至於記憶法。
快用你那所向無敵的白金之星告訴我三者在幹嘛!
細心的讀者可以發現說,雖然依照字母順序abc很順,但我的標題卻打著cab,那到底是什麼陰謀的。
這個問題可以分類成 [ call , apply ] vs [ bind ] 來做探討。
- call、apply皆是回傳function執行結果
- bind方法回傳的是綁定 this 後的原函數
我們可以從這個觀察中發現,bind()想完成的事有根本上的差別,但是將this正確的bind進去之後馬上執行,意義上是一樣的。
來詳細說說call()的用法
使用給定的
this
參數以及分別給定的參數來呼叫某個函數 (src. MDN)
MDN上的解釋其實有點術語化,我當時也是看的挺懵的,如果要我用自己的話說的話,我認為call這個方法並不難於解釋
任何一個function.call()的狀況下,我們可以把整個參數陣列( arguments )向右平移,而第零個參數則是告訴函數我們想使用的 this 。
fun.call(thisArg[, arg1[, arg2[, ...]]])
也就是說整個函數除了第零個變數以外,其他跟原函數87%像,直接舉例如下。
func( 1 , 2 , 3 ) vs func.call( null , 1 , 2 , 3 )
如果理解了上述用法,那麼apply()就只是一個shortcut
fun.apply(thisArg, [argsArray])
apply()的接口只允許兩個變數傳入,第二個變數等於是原始function的參數陣列。
func( 1 , 2 , 3 ) vs func.apply( null , [ 1 , 2 , 3 ] )
說說特別的點:
- JavaScript 有一個神奇的東西叫做 arguments,這個變數引用了函數被呼叫時傳入的參數陣列,操作完的陣列不需解構就能apply回去。
小聲說:上頭的例子,其實用 bind 一下就做掉了。
看起來 call 與 apply 想解決的事情挺相似的 — 那麼 bind() 呢?
一眼望向網際網路,你會發現只要 bind 跟 curry 一起丟下去搜索,可以找到成千上萬個文章在介紹如何用 bind 來 currying 、科里化多麼棒之類的文章。
我們就來認真探討一下 bind ,到底是想 bind 什麼了什麼東西。
「bind」,Google 翻譯做「捆綁」,因為諧音的關係,我們以下簡稱為「綁」。用這個中文字,想必他要做的事就那麼簡單地心領神會了。
bind — 這個方法與前兩者不同,使用這個方法,並不需要給與對應的參數或參數陣列,然而他也不會回傳你函數執行結果(廢話?),而是給你一個「綁定this的函數」,對,他把this給綁了進去,一但被綁定,其this無法再被修改。
很容易理解的是,不管函數外頭被裹上了什麼樣的糖衣,最終 wtf_bind_window 還是會把 window 給綁進 wtf。
用一個 Case 來討論你真的需要bind()嗎?
我們先來討論一個很典型,只要寫過 callback 都有可能發生的 case 。
實際執行我們可以發現 laterHello 的這個方法,他的 timer callback 拿不到正確的 this ,這種不管是在 Node 的 class , Vue 、 React 的組件都爆幹常遇到的問題。
我這邊有三種思路可以跟大家分享,第一是最常看到的是 self 大法,沒有搞懂動態 this 的人約莫會以為這招在饒舌…
1. Self大法駕到 — this 的替身攻擊
維護 legacy 專案最常看到的就是這種解法
不過他會是第一高票的解法其中原因是,第二種解法需要考驗開發者了不了解c/a/b三神器,而第三種解法但是還沒有出生。
- 優點 是能解決問題 (哈哈 笑死)
- 缺點 回調函數會與外層 self 緊耦合
2. Binding大法 — 綁定大法
這種做法有個非常強大的優點,就是不需要改變回調函數本身的區塊,而是在外頭直接告訴callback該用哪個作為正確的this。
缺點是不是每個人都看得懂(不過當你看完這篇文就懂了!),還有就是在class裡頭不推薦這麼做,因為ES6有個也不錯的方法。
3. Arrow Function — 箭頭通通給我指起來
JavaScript最神奇的地方在於,當你不想了解某個特性,你甚至可以放棄它,有完全不同的 pattern 可以讓你不需要用到這個不了解的特性。 — this
箭頭函數 多數時候一般函數無異,但是最大的差別在於 — 其 this 完全綁定在語彙上的位置,也就是說在 arrow 裡面的 this 永遠都是語意上的 this ,不管是誰呼叫他,或是被如何 bind 、 call 、 apply ,他永遠都是拿到原先作用域的 this 。
這樣的做法可以讓你在物件上使用回調時,拿到正確的 this ,而非一言不合就 undefined ,不過需要注意的是,不是每個環境都可以接受 ES6+( why? ),進入生產模式時,還是 babel 一波比較保險。
寫在最後
終於寫完這篇文章了,寫文章最棒的收穫,還是讓自己更通透的陳述題目,畢竟講給別人懂,會比說給自己聽更加詳細。
也因為這次的文章量比較長,所以加入了一些梗圖,讓閱讀上不會這麼想按上一頁,如果有其他建議也可以告訴我,謝謝啦。