JS call、apply、bind的巧妙用法

2022-03-15 19:00:30
call() 函數、apply() 函數、bind() 函數有一些巧妙的用法,能夠快速實現一些效果,這裡我們總結出了 5 個場景。

求陣列中的最大項和最小項

Array 陣列本身沒有 max() 函數和 min() 函數,無法直接獲取到最大值和最小值,但是 Math 卻有求最大值和最小值的 max() 函數和 min() 函數。

我們可以使用 apply() 函數來改變 Math.max() 函數和 Math.min() 函數的執行主體,然後將陣列作為引數傳遞給 Math.max() 函數和 Math.min() 函數。
var arr = [3, 5, 7, 2, 9, 11];
// 求陣列中的最大值
console.log(Math.max.apply(null, arr));  // 11
// 求陣列中的最小值
console.log(Math.min.apply(null, arr));  // 2
apply() 函數的第一個引數為 null,這是因為沒有物件去呼叫這個函數,我們只需要這個函數幫助我們運算,得到返回結果。

第二個引數是陣列本身,就是需要參與 max() 函數和 min() 函數運算的資料,運算結束後得到返回值,表示陣列的最大值和最小值。

類陣列物件轉換為陣列物件

函數的引數物件 arguments 是一個類陣列物件,自身不能直接呼叫陣列的方法,但是我們可以藉助 call() 函數,讓 arguments 物件呼叫陣列的 slice() 函數,從而得到一個真實的陣列,後面就能呼叫陣列的函數。

任意個數位的求和的程式碼如下所示:
// 任意個數位的求和
function sum() {
    // 將傳遞的引數轉換為陣列
    var arr = Array.prototype.slice.call(arguments);
    // 呼叫陣列的reduce()函數
    return arr.reduce(function (pre, cur) {
        return pre + cur;
    }, 0)
}

sum(1, 2);       // 3
sum(1, 2, 3);    // 6
sum(1, 2, 3, 4); // 10

用於繼承

// 父類別
function Animal(age) {
    // 屬性
    this.age = age;
    // 範例函數
    this.sleep = function () {
        return this.name + '正在睡覺!';
    }
}
// 子類
function Cat(name, age) {
    // 使用call()函數實現繼承
    Animal.call(this, age);
    this.name = name || 'tom';
}

var cat = new Cat('tony', 11);
console.log(cat.sleep());  // tony正在睡覺!
console.log(cat.age);  // 11
其中關鍵的語句是子類中的 Animal.call(this, age),在 call() 函數中傳遞 this,表示的是將 Animal 建構函式的執行主體轉換為 Cat 物件,從而在 Cat 物件的 this 上會增加 age 屬性和 sleep 函數,子類實際相當於如下程式碼:
function Cat(name, age) {
    // 來源於對父類別的繼承
   this.age = age;
    this.sleep = function () {
        return this.name + '正在睡覺!';
    };
    // Cat自身的範例屬性
    this.name = name || 'tom';
}

執行匿名函數

假如存在這樣一個場景,有一個陣列,陣列中的每個元素是一個物件,物件是由不同的屬性構成,現在我們想要呼叫一個函數,輸出每個物件的各個屬性值。

我們可以通過一個匿名函數,在匿名函數的作用域內新增 print() 函數用於輸出物件的各個屬性值,然後通過 call() 函數將該 print() 函數的執行主體改變為陣列元素,這樣就可以達到目的了。
var animals = [
    {species: 'Lion', name: 'King'},
    {species: 'Whale', name: 'Fail'}
];
for (var i = 0; i < animals.length; i++) {
    (function (i) {
        this.print = function () {
            console.log('#' + i + ' ' + this.species + ': ' + this.name);
        };
        this.print();
    }).call(animals[i], i);
}
在上面的程式碼中,在 call() 函數中傳入 animals[i],這樣匿名函數內部的 this 就指向 animals[i],在呼叫 print() 函數時,this 也會指向 animals[i],從而能輸出 speices 屬性和 name 屬性。

bind()函數配合setTimeout

在預設情況下,使用 setTimeout() 函數時,this 關鍵字會指向全域性物件 window。當使用類的函數時,需要 this 參照類的範例,我們可能需要顯式地把 this 繫結到回撥函數以便繼續使用範例。
// 定義一個函數
function LateBloomer() {
    this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 定義一個原型函數
LateBloomer.prototype.bloom = function () {
    // 在一秒後呼叫範例的declare()函數,很關鍵的一句
    window.setTimeout(this.declare.bind(this), 1000);
};
// 定義原型上的declare()函數
LateBloomer.prototype.declare = function () {
    console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};
// 生成LateBloomer的範例
var flower = new LateBloomer();
flower.bloom();  // 1秒後,呼叫declare()函數
在上面的程式碼中,關鍵的語句在 bloom() 函數中,我們期望通過一個定時器,設定在 1 秒後,呼叫範例的 declare() 函數。很多人可能會寫出下面這樣的程式碼。
LateBloomer.prototype.bloom = function () {
    window.setTimeout(this.declare, 1000);
};
此時,當我們呼叫 setTimeout() 函數時,由於其呼叫體是 window,因此在 setTimeout() 函數內部的 this 指向的是 window,而不是物件的範例。

這樣在 1 秒後呼叫 declare() 函數時,其中的 this 將無法存取到 petalCount 屬性,從而返回“undefined”,輸出結果如下所示:

I am a beautiful flower with undefined petals!

因此我們需要手動修改 this 的指向,而通過 bind() 函數能夠達到這個目的。

通過 bind() 函數傳入範例的 this 值,這樣在 setTimeout() 函數內部呼叫 declare() 函數時,declare() 函數中的 this 就會指向範例本身,從而就能存取到 petalCount屬性。
LateBloomer.prototype.bloom = function () {
    window.setTimeout(this.declare.bind(this), 1000);
};
執行程式碼,在 1 秒後,得到的結果如下所示:

I am a beautiful flower with 4 petals!