我們昨天有提到,在JavaScript裡,this
的指向取決於我們呼叫他的方式,我們總共有4種呼叫方式:
- 一般呼叫:當你以一般方式呼叫一個function 時,例如剛剛的案例
setName**()**
,**this
** 會指向 **window
**(在瀏覽器環境中)。 - 物件方法呼叫:當你將一個function 作為 object 的方法來呼叫,例如剛剛的案例
c.log()
,this
就會指向 c 這個object。 - **使用
.bind()
、.call()
或.apply()
**:你可以使用這些方法來明確設定function 的this
值。這個會在下一章節介紹。 - 箭頭函式:箭頭函式 (
=>
) 不會改變this
的值,或者這麼說:箭頭函式 (=>
) 不會像傳統函數那樣有自己的 **this
**,而是會捕獲(capture)它們外部作用域的 **this
**。這使得箭頭函式在定義時捕獲了this
的值,不受呼叫方式的影響。
今天我們要針對第三點來做一次解說。
Function 的調用
在之前我們調用function都是用:
1 | func(p1, p2) |
但其實還有一種方法,甚至可以說前面兩種方法都只是這種寫法的語法糖。
1 | func.call(context, p1, p2) |
這種調用方式才是正常調用方式,我們可以看看前兩種方式如何轉換成這樣
1 | func(p1, p2) |
其實當我們都用這種方式來調用function,要了解this就非常簡單的。
那你可能會問不對啊,第一個案例的context是帶入undefined啊,this怎麼會是undefined。
還記得昨天有說到:
一但脫離了物件導向,其實 this 就沒有什麼太大的意義,因為:
- 嚴格模式底下就都是
undefined
- 非嚴格模式,瀏覽器底下是
window
- 非嚴格模式,node.js 底下是
global
所以當我們在瀏覽器底下運作時,當我們傳入的context是null
或undefined
時,window
就會是默認的context。
希望這段可以讓大家更了解this。
First Class Functions
在介紹今天的主題前,我們要先複習一下First Class Functions
:
First Class Functions
並不是 JavaScript 專有的特性,只要該語言的「函式可被視為與其它變數一樣時」,就可以稱為該語言有First Class Functions
的特性。
這些 JavaScript 的函式就具有以下特性:
- 將函式指定到一個變數
- 函式可作為參數來傳遞
- 函式也可以被回傳(return)
- 函式也是物件的物件(傳參考特性、具有屬性)
因此我們可以知道,function它只是一種特殊的object
,而它包含了兩個隱藏的屬性,一個是name property,用來儲存函式的名稱(也可以是匿名函式);另一個是code property,用來儲存函式當中程式碼的內容。
認識這3個method
就像剛剛說的,function他只是一種特殊的object
所以它具有properties 和 methods。剛剛提到的call
、apply
和bind
就是function裡面的method。
看一下下面這段code:
1 | var person = { |
這段程式碼執行後會出現什麼?如果this觀念有學好應該馬上就可以知道會出現錯誤,原因就在於global object根本沒有getFullName這個方法,這個方法是被建立在person這個物件裡面。所以就會出現:
1 | TypeError: this.getFullName is not a function |
但如果我們想要指稱logName function中this所指稱的對象,這時候就可以使用bind
。
bind
只要在JavaScript中建立的函式,都會預設有bind這個方法在內。使用的方法只要在該function後使用.bind,並於( )的地方代入欲替換成this的物件。
這裡要特別注意,不是寫成這樣:logName().bind(person)
。因為logName()
會是執行後的結果,而bind(person)
卻是function的method,所以正卻的寫法應該是:var logPersonName = logName.bind(person)
然後在logPersonName()
。
1 | var logName = function(lang1, lang2) { |
也可以直接在logName函式的後面去執行 bind,如下:
1 | var logName = function(lang1, lang2) { |
call
call的用法其實就和括號 ( ) 一樣,都是直接去執行invoke
這個function,但不一樣的地方在於,call的後面可以帶入你想要指定this的物件,接著再放入參數。
我們使用剛剛的範例來舉例:
1 | var person = {...} |
apply
apply和call的用法大同小異,唯一不同的地方在於,使用apply時,放入參數的地方,應該要放入的是陣列(array)。講師有提到apply的用法常用到有許多算數的地方。
1 | var person = {...} |
實例
bind是複製原本的函式,並且將你所指定的this代入這個函式中,所以如果你要在執行這個函式的話,最後要接上( )來執行該函式;而call和apply則是將你所指定的this直接代入該function中並執行,所以最後面不用在加上( )來執行該函式。
理論知道後,來看一些實際案例:
Function Borrowing
假設我們現在建立了另一個物件叫做person2,但我想要使用person這個物件裡面的getFullName這個方法時,我該怎麼處裡?其實非常簡單:
1 | var person2 = { |
首先我們要先找到getFullName,然後用apply()或是call(),裡面的參數則是放我們要放入的person2就可以了。
Function Currying
1 | function multiply(a, b) { |
我們需要一個2個數相乘的函式,取名叫multiply並設定需要兩個參數。
這時候我需要a都是固定數值但不要更改到multiply這個函式的內容,所以我們使用了var multipleByTwo = multiply.bind(this, 2)
這個寫法,這寫法的意思就是說我們把a這個參數設定成2,然後複製原本multiply這個function變成multipleByTwo。
所以之後執行multipleByTwo這個函式,就只需要代入一個參數(原本multiply裡的參數b;a已經預設為2)
這時候如果這時候我把這段改成multiply.bind(this, 2, 5)
,就是把 a 的參數設定成2,把 b 的參數設定成 5 。
Functional Programming
1 | var arr1 = [1, 2, 3] |
這是一段簡單的code,我需要一個陣列arr2,而裡面的元素都是arr1 x 2。
但在AC上課時有說到:身為一個工程師,我們總是想要透過最少的程式碼來達到同樣的效果,而且要避免類似的程式碼重覆。
因此這邊課程教導了我一個概念:Functional Programming
第一步,就是先把程式碼函式化,把會變動的值當成參數處理:
1 | function mapForEach(arr, fn) { |
這邊我們帶入的參數中,第一個是要放入的陣列,取名叫arr;第二個就是要放入要執行的函式,取名叫fn。
接下來就是放入要放入的內容,如下:
1 | var arr1 = [1,2,3]; |
結果就會是我們想要的[2, 4, 6]。
有了mapForEach
這個function後我們就可做很多的延伸變化,例如:
判斷在arr1這個陣列中,有哪些元素值是大於2的:
1 | var arr3 = mapForEach(arr1, function(item) { |
那如果我們不想放function expression而是想放變數呢?
1 | var checkPastLimit = function(limiter, item) { |
為什麼這邊要用到bind?原因就在於mapForEach
這個函式裡面的newArr.push(fn(arr[i]))
這段,fn裡面只會帶入一個參數,但我們在var checkPastLimit = function(limiter, item){...}
卻是要帶入兩個參數。
因此為了解決這個問題,我們需要將其中一個參數變成預設值,因此才會使用bind。
那再往下延伸,如果我也不想每次都要使用bind,我只想每次帶入limiter
這個參數,可以嗎?
也是可以,如下:
1 | var checkPastLimitSimplified = function(limiter) { |
甚至可以再更簡化成:
1 | var checkLimiterSimplified = function(limiter) { |
這邊簡單做一個總結:
call
、apply
和 bind
這三個方法都用於處理 JavaScript 函式中的 this
值以及傳遞參數,但它們之間存在一些重要的差異。
- 共同點:
- 這三個方法都用於調用函式,並允許指定函式內部的
this
值。
- 這三個方法都用於調用函式,並允許指定函式內部的
- 差異:
call
方法:call
是用於立即調用函式的方法。- 第一個參數指定了函式內部的
this
值。 - 後續的參數是用來傳遞給函式的參數,可以是一個一個的值。
apply
方法:apply
也是用於立即調用函式的方法。- 第一個參數指定了函式內部的
this
值。 - 第二個參數是一個陣列,其中包含了要傳遞給函式的參數。
bind
方法:bind
方法不是立即執行函式,而是創建一個新的函式。bind
用來指定函式內部的this
值以及預先設定的參數。- 返回的是一個新函式,你可以在需要時再次調用它,而且它的
this
值和參數已經預先設定好了。
簡而言之,**call
** 和 apply
用於立即調用函式並傳遞參數,而 bind
用於創建一個新的函式,這個函式在稍後調用時具有預先設定的 this
值和參數。
參考資料: