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