Jeff的隨手筆記

學習當一個前端工程師

0%

『JavaScript 基礎』 認識call( ), apply( )和bind( )

Imgur

這是一門在Udemy 課程,是同學介紹的。主要是因為想要運用到過年前這段時間好好的增進自己的JavaScript的基礎能力,讓自己能往前端工程師更近一步。主要還是會以筆記的形式做呈現!


在之前的有提到過,我們在執行環境中會有:Variable Environment、Outer Environment、this。
其中this某些情況是會指向global environment,某些時候則是指向object
Imgur

但有些時候我們必須要去control 函式裡面的this所指向的object有辦法嗎?
是有辦法的,就是我們現在要介紹的callapplybind

|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。剛剛提到的callapplybind就是function裡面的method。

看一下下面這段code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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這個物件裡面。所以就會出現:

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
2
3
4
5
6
7
var logName = function(lang1, lang2) {

console.log('Logged: ' + this.getFullName());

}
var logPersonName = logName.bind(person)
logPersonName()

也可以直接在logName函式的後面去執行 bind,如下:

1
2
3
4
5
6
7
var logName = function(lang1, lang2) {

console.log('Logged: ' + this.getFullName());

}.bind(person)

logName()

|call

call的用法其實就和括號 ( ) 一樣,都是直接去執行invoke這個function,但不一樣的地方在於,call的後面可以帶入你想要指定this的物件,接著再放入參數。

我們使用剛剛的範例來舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

apply和call的用法大同小異,唯一不同的地方在於,使用apply時,放入參數的地方,應該要放入的是陣列(array)。講師有提到apply的用法常用到有許多算數的地方。

1
2
3
4
5
6
7
8
9
10
11
12
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中並執行,所以最後面不用在加上( )來執行該函式。

理論知道後,來看一些實際案例:

|Function Borrowing

假設我們現在建立了另一個物件叫做person2,但我想要使用person這個物件裡面的getFullName這個方法時,我該怎麼處裡?其實非常簡單:

1
2
3
4
5
6
7
var person2 = {
firstname: 'Jane',
lastname: 'Doe'
}

console.log(person.getFullName.apply(person2));

首先我們要先找到getFullName,然後用apply()或是call(),裡面的參數則是放我們要放入的person2就可以了。

|Function Currying

1
2
3
4
5
6
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 。

|Functional Programming

1
2
3
4
5
6
7
8
9
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

第一步,就是先把程式碼函式化,把會變動的值當成參數處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
function mapForEach(arr, fn) {

var newArr = [];
for (var i = 0; i < arr.length; i++) {

newArr.push(
fn(arr[i])
);
};

return newArr;

}

這邊我們帶入的參數中,第一個是要放入的陣列,取名叫arr;第二個就是要放入要執行的函式,取名叫fn。

接下來就是放入要放入的內容,如下:

1
2
3
4
5
6
7
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的:

1
2
3
4
var arr3 = mapForEach(arr1, function(item) {
return item > 2;
});
console.log(arr3);

那如果我們不想放function expression而是想放變數呢?

1
2
3
4
5
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這個參數,可以嗎?

也是可以,如下:

1
2
3
4
5
6
7
8
var checkPastLimitSimplified = function(limiter) {
return function(limiter, item) {
return item > limiter;
}.bind(this, limiter);
};

var arr5 = mapForEach(arr1, checkPastLimitSimplified(1));
console.log(arr5);

甚至可以再更簡化成:

1
2
3
4
5
6
7
8
9
10
var checkLimiterSimplified = function(limiter) {

return function(item) {
return item > limiter;
}

}

var arr6 = mapForEach(arr1, checkLimiterSimplified(1));
console.log(arr6);