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之間差異的展示。
由於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
如果對文章內容有任何問題或是錯誤,歡迎在底下留言讓我知道: )