iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
自我挑戰組

重新複習JavaScript系列 第 25

[Day -25] 認識call( ), apply( )和bind( )

  • 分享至 

  • xImage
  •  

我們昨天有提到,在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都是用:

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
  • 非嚴格模式,node.js 底下是global

所以當我們在瀏覽器底下運作時,當我們傳入的context是nullundefined時,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。剛剛提到的callapplybind就是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

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

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

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中並執行,所以最後面不用在加上( )來執行該函式。

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

Function Borrowing

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

var person2 = {
    firstname: 'Jane',
    lastname: 'Doe'
}

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

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

Function Currying

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

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);

這邊簡單做一個總結:

callapplybind 這三個方法都用於處理 JavaScript 函式中的 this 值以及傳遞參數,但它們之間存在一些重要的差異。

  1. 共同點
    • 這三個方法都用於調用函式,並允許指定函式內部的 this 值。
  2. 差異
    • call 方法
      • call 是用於立即調用函式的方法。
      • 第一個參數指定了函式內部的 this 值。
      • 後續的參數是用來傳遞給函式的參數,可以是一個一個的值。
    • apply 方法
      • apply 也是用於立即調用函式的方法。
      • 第一個參數指定了函式內部的 this 值。
      • 第二個參數是一個陣列,其中包含了要傳遞給函式的參數。
    • bind 方法
      • bind 方法不是立即執行函式,而是創建一個新的函式。
      • bind 用來指定函式內部的 this 值以及預先設定的參數。
      • 返回的是一個新函式,你可以在需要時再次調用它,而且它的 this 值和參數已經預先設定好了。

簡而言之,callapply 用於立即調用函式並傳遞參數,而 bind 用於創建一個新的函式,這個函式在稍後調用時具有預先設定的 this 值和參數。

參考資料:

https://zhuanlan.zhihu.com/p/23804247


上一篇
[Day -24] this
下一篇
[Day -26] 原型、繼承和原型鍊
系列文
重新複習JavaScript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言