You are currently viewing [筆記]-JavaScript bind、call、apply是什麼?關於他們的4個重點

[筆記]-JavaScript bind、call、apply是什麼?關於他們的4個重點

this的指向是JavaScript中常常讓人感到頭大的問題,其中緣由便是this是動態的。這篇筆記中ㄚ建幫你整理介紹指定this的實用方法bind、call、apply,以及這3者之間的關係與使用方式。

  • bind、apply、call是什麼?
  • bind、apply、call的特性是什麼?
  • 如何使用bind、apply、call?

為何需要bind、apply、call?

JavaScript中this的指向是個容易讓人混淆的事情,由於this是動態的,如果對其概念不夠清楚便無法取得正確的訊息,而bind、call、apply就是為了動態改變的this而出現的。

先看這個例子:

const userInfo = {
    userName: "小美",
    age: function () {
        return `${this.userName} is 18`;
    }
};
const showAge = userInfo.age;
console.log(showAge()); // undefined is 18

上面的例子由於this指向錯誤所以沒法正確拿到userName的值。
讓我們在上面的例子加入下面的程式碼。

const showAge_bind = showAge.bind(userInfo);
const showAge_apply = showAge.apply(userInfo);
const showAge_call = showAge.call(userInfo);
console.log(showAge_bind()); // 小美 is 18
console.log(showAge_apply); // 小美 is 18
console.log(showAge_call); // 小美 is 18

透過bind、apply、call我們可以改變this的指向,將this指向到userInfo中,因此可以正確取得userName的值。
這三者主要功能便是將this正確指向到目標。

觀察上面例子的結果,我們可以發現這三者具有相同的能力去完成同一件事。
既然它們可以做相同的事,那麼它們的區別在哪?
接下來讓我們來看看bind、apply、call在用法上的區別吧!

apply與call的用法與區別

apply語法(MDN)

fun.apply(thisArg, [argsArray])
  • 參數1:this指向的目標函式。
  • 參數2:必須是一個 array-like object,如果不需要提供陣列,可以傳入 null 或 undefined。

call語法(MDN)

function.call(thisArg, arg1, arg2, ...)
  • 參數1:this指向的目標函式。
  • 參數2:會作為目標函式的參數傳入,如果目標函式不需要參數則可以不傳入。

apply與call的區別

apply與call二者的作用完全一樣,只是第二個參數的接受方式有所區別。

call的第2個參數需要把參數依序傳入,就如同函式的參數。
apply的第2個參數則必須是一個 array-like object,也就是參數必須以陣列形式傳入。

使用的例子如下

const person = {
  fullName: function(city, country) {
    return this.firstName + " " + this.lastName + "," + city + "," + country;
  }
}

const person1 = {
  firstName:"John",
  lastName: "Doe"
}
person.fullName.call(person1, "Oslo", "Norway");
person.fullName.apply(person1, ["Oslo", "Norway"]);

bind的用法

  • 參數1:this指向的目標函式。
  • 參數2:會作為目標函式的參數傳入,如果目標函式不需要參數則可以不傳入。
function.bind(thisArg, arg1, arg2, ...)

apply、call與bind的差異

  • apply與call會直接回傳函式的執行結果。
  • bind是創建一個新的綁定函式,這個函式包裝了原本的函式,並且與第一個參數的this綁定。

bind是回傳一個函式,而apply與call綁定的函式會被立即執行,所以會直接得到函式的執行結果。bind與apply、call最大的差別在於此。

如果覺得不夠也可以看這部影片對apply、call與bind之間差異的展示。

兩分鐘說完call, apply和bind

由於bind回傳的函式不會立刻執行,所以在使用上相對於apply、call便有更大的彈性。
經常聽到的currying(柯里化)在使用上經常與bind結合,幫助我們優化許多程式的寫法。

柯里化是什麼?

首先讓我們來看柯里化的定義,這邊參考維基百科的定義:

柯里化

在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。

簡單來說,柯里化主要目的就是封裝函式的接口,讓接口便的單純使程式碼更容易理解,柯里化目的只要把握2個重點就可以了

  • 簡化參數的處理,讓函式基本只處理一個參數
  • 讓程式有助於重複利用,增加程式的可讀性與彈性

而柯里化需要封裝函式的接口,這便會運到到閉包的概念,如果對閉包的概念還不清楚,可以先參考這篇文章

讓我們先建立一個印出球隊與其成員的函式。

const baseballTeam = (team, member) => {
    return team + ':' + memberList
};
const team_MONKEYS = baseballTeam("樂天桃猿", "霸林爵");
console.log(team_MONKEYS); // 樂天桃猿:霸林爵

上面例子中簡單建立了一個球隊與其成員的資料,baseballTeam需要2個參數才可以正常執行。
接下來透過柯里化重構baseballTeam的接口,使函式只要輸入球隊名稱,並返回接受球員名稱的函式,結果如下:

const baseballTeamCurried = (team) => {
    return function (member) {
        console.log(team + ':' + member);
    }
};
const team_MONKEYS = baseballTeamCurried("樂天桃猿");
team_MONKEYS("霸林爵"); // 樂天桃猿:霸林爵
const team_BROTHERS = baseballTeamCurried("中信兄弟");
team_BROTHERS("象魔力"); // 中信兄弟:象魔力

透過柯里化的調整,baseballTeamCurried可以建立各式球隊名稱,並且將所屬的球隊成員加入返回的函式。
baseballTeamCurried調整後我們可以輕易了解函式的目的,而需要建立新球隊時,也不需要額外加入其他程式,這讓整個函式可以重複利用。

我們還可以進一步延伸讓函式可以紀錄隊伍的球員名單:

const baseballTeamCurried = (team) => {
    let memberList = [];
    return function (member) {
        memberList.push(member);
        console.log(team + ':' + memberList);
    }
};
const team_MONKEYS = baseballTeamCurried("樂天桃猿");
team_MONKEYS("霸林爵"); // 樂天桃猿:霸林爵
team_MONKEYS("許峻暘"); // 樂天桃猿:霸林爵,許峻暘
const team_BROTHERS = baseballTeamCurried("中信兄弟");
team_BROTHERS("象魔力"); // 中信兄弟:象魔力
team_BROTHERS("吳哲源"); // 中信兄弟:象魔力,吳哲源

bind與柯里化的應用

在更為複雜的函式應用,有時需要透過this來處理相關函式,確保函式中this的指向尤為重要,這時bind就可以幫助我們完成這件事。

在下面的例子實現了bind的柯里化的計算機,透過bind綁定methods的運算方式,再由計算機對外的接口參數operation決定計算機的運算行為。

const calculator = (operation) => {
    const methods = {
        add: function (numbers) {
            let result = 0;
            for (let number of numbers) {
                result += number;
            }
            console.log("result:", result);
        },
        sub: function (numbers) {
            let result = 0;
            for (let number of numbers) {
                result -= number;
            }
            console.log("result:", result);
        },
    }
    function calc_operation(operation, ...numbers) {
        if (operation === 'add') {
            return this.add(numbers);
        } else {
            return this.sub(numbers);
        }
    }
    if (operation === "add" || operation === "sub") {
        return calc_operation.bind(methods, operation);
    } else {
        throw "error operation!";
    }
}
const add_calculator = calculator("add");
add_calculator(1, 2); // result: 3
const sub_calculator = calculator("sub");
sub_calculator(2, 3); // result: -5

總結

總結一下這篇文章中我們獲得了什麼?

bind、apply、call的特性

  • 3者皆是改變函式中this的指向。
  • 3者第一個參數皆是要綁定的this
  • 3者皆可以傳遞參數
  • bind會創建一個新的綁定函式,並返回該函式。
  • apply、call會立即執行綁定的函式。

柯里化

  • 簡化參數的處理,讓函式基本只處理一個參數。
  • 讓程式有助於重複利用,增加程式的可讀性與彈性。

參考資料

JavaScript – The Complete Guide
MDN-bind
MDN-apply
[JavaScript] 函數原型最實用的 3 個方法 — call、apply、bind
A Beginner’s Guide to Currying in Functional JavaScript
Why Curry Helps

如果對文章內容有任何問題或是錯誤,歡迎在底下留言讓我知道: )

發佈留言