雖然 Day8 的文章有提到 call()、bind()、apply() 這三個函式,不過也就只是淺淺帶過它們都能改變 this 指向,所以接下來的幾篇文章要來詳細的介紹它們,今天會先介紹 call() 和 apply()。
第一個參數傳入的值會作為該函式 this 指向,假設沒有傳入,在瀏覽器環境下指向 window 物件。
第二個參數以後的參數會作為參數傳進目標函式中,如果函式不需要參數則不要傳入即可。
和 call() 用法幾乎一樣,差異在於它的第二個參數是陣列,第二個參數可加可不加。
call() & apply() 的應用眾多,這裡舉幾個例子來說明。
透過 call() 去借用其他物件的函式:
Function Borrowing
const wizard = {
name: "Jack",
health: 100,
heal(num1, num2) {
return (this.health += num1 + num2);
}
};
const warrior = {
name: "Reiner",
health: 30
};
console.log(warrior); // health: 30
wizard.heal.call(warrior, 50, 20);
console.log(warrior); // health: 100
字串當作 call 的參數傳入,這種情況只適合用在不會修改到字串的純粹函式:
const data1 = 'Jordan Smith';
const data2 = [].filter.call(data1, function(ele, index) {
return index > 6;
});
console.log(data2); // ["S", "m", "i", "t", "h"]
在 MDN 介紹 Object.prototype.toString() 的文件有段落提到可以使用 Object.prototype.toString()
搭配 call() 去偵測一個值的類別
const toString = Object.prototype.toString;
toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]
// Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
根據上段程式碼,我們可以寫出幾個判斷型別的函式:
const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]';
const isArray = (obj) => Object.prototype.toString.call(obj) === '[object Array]';
const isNumber = (obj) => Object.prototype.toString.call(obj) === '[object Number]';
透過高階函式改寫:
const isType = type => obj => Object.prototype.toString.call(obj) === '[object ' + type + ']';
isType('String')('123'); // true
isType('Array')([1, 2, 3]); // true
isType('Number')(123); // true
將一個陣列的元素加到另一個陣列:
const arr1 = ['a', 'b'];
const arr2 = [0, 1, 2];
arr1.push.apply(arr1, arr2);
console.log(arr1); // ["a", "b", 0, 1, 2]
透過重複造輪子的過程能幫助自己更了解 JS,所以以下要來實作 call() 這個函式,首先我們先看一段使用 call() 的程式碼,經觀察後可以知道兩點:
function showName(...args) {
console.log(`${args} ${this.name}.`);
}
const person = { name: 'Harry' };
showName.call(person, 'My name is'); // 'My name is Harry.'
以下的內容說明為了不占篇幅,會省略 showName 函式和 person 物件的程式碼。
根據上述兩點,先初步寫出 myCall() 函式,帶入兩個參數,並且透過兩個 console 可以看到 myCall() 函式裡印出的值,如以下註解:
Function.prototype.myCall = function(thisArg, ...args) {
console.log(thisArg); // {name: 'Harry'}
console.log(this); // function showName {...}
// this 指向調用該函式的物件,showName 函式也是物件
}
showName.call(person, 'My name is');
接著思考: 如果 person 物件有 showName 函式的話,是不是就可以順利的印出想要的結果?
const person = {
name: 'Harry',
showName(...args) {
console.log(`${args} ${this.name}.`);
}
};
person.showName('My name is'); // 'My name is Harry.'
將想法加入到實作的 myCall() 函式內:
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this; // person 物件加入 showName 函式
thisArg.fn(...args); // 呼叫 showName 函式
delete thisArg.fn; // 最後將原本不存在 person 物件的 showName 函式移除
}
showName.myCall(person, 'My name is'); // 'My name is Harry.'
這樣就完成我們初步版本的 call() 了!
但上面版本的 myCall() 函式有三個狀況不能處理:
讀者可以觀看以下範例,就知道上面三點在程式運作上的問題。
function showName(...args) {
return `${args} ${this.name}.`; // 改寫成回傳字串
}
const person = { name: 'Harry' };
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this;
thisArg.fn(...args);
delete thisArg.fn;
}
// 1. myCall() 的第一個參數若傳入 null 或是 undefined 會出錯
showName.myCall(null, 'My name is'); // Cannot set properties of null
// 2. 不能處理原始型別,例如數字
const isNumber = (obj) => Object.prototype.toString.myCall(obj) === '[object Number]';
console.log(isNumber(2)); // Cannot read properties of undefined
// 3. 沒有處理到呼叫函式的回傳值
const result = showName.myCall(person, 'My name is');
console.log(result); // undefined
接著就來動手改良吧!
因為原生的 call() 函式的第一個參數 thisArg 傳入 null 或是 undefined 的話,this 會指向 window,所以我們可以判斷傳入的參數是否存在,不存在的話就將 thisArg 賦值 window。
Function.prototype.myCall = function(thisArg, ...args) {
thisArg = thisArg ? thisArg : window;
thisArg.fn = this;
thisArg.fn(...args);
delete thisArg.fn;
}
將上面的那行 thisArg = thisArg ? thisArg : window;
改寫成 thisArg = thisArg ? Object(thisArg) : window;
,透過 Object()
去將傳入的參數轉成物件,像有問題的範例中 isNumber(2)
傳入數字 2,轉換後變成截圖的樣子:
最後就是處理呼叫函式沒回傳的值,所以將執行結果存成變數回傳即可。
Function.prototype.myCall = function(thisArg, ...args) {
thisArg = thisArg ? Object(thisArg) : window;
thisArg.fn = this;
const result = thisArg.fn(...args);
delete thisArg.fn;
return result;
}
到第二版的 myCall() 函式已經能處理大部分情況了,但當然還有可以再進一步考慮優化的。
當嚴格模式下 thisArg 值為 null/undefined 時,this 不應該讓它指向 window 而是 undefined,所以要加上判斷是否嚴格模式的程式碼。
判斷是否嚴格模式可以用這段程式碼:const isStrict = (function(){ return this === undefined }())
然後如果試著在嚴格模式下用原生 call() ,並且第一個參數為 null/undefined 時去呼叫函式的話 showName.call(null, 'My name is');
,會跳出錯誤 Cannot read properties of...
。
所以加上 if (!thisArg) this(...args);
,直接呼叫函式,此範例的話就是直接呼叫 showName 函式。
Function.prototype.myCall = function(thisArg, ...args) {
const isStrict = (function(){ return this === undefined }());
thisArg = thisArg ? Object(thisArg) : (isStrict ? undefined : window);
if (!thisArg) this(...args);
thisArg.fn = this;
const result = thisArg.fn(...args);
delete thisArg.fn;
return result;
}
最後這就是完成的樣子,apply() 的實作也打同小異,這裡就交給讀者試試囉~明天將要來介紹 bind()。