理解JS函式之call,apply,bind

CodeForBetter發表於2023-02-24

前言

在 JavaScript 中,apply、bind 和 call 是三個重要的函式,它們都是 Function.prototype 的方法。這些函式可以讓我們動態地改變函式的 this 值,或者傳遞引數來執行函式。本篇部落格將詳細介紹 apply、bind 和 call 的使用方法以及它們之間的區別。

apply

apply() 方法是 Function.prototype 上的一個方法,可以用於改變函式的 this 值。它接受兩個引數:第一個引數是要繫結的 this 值,第二個引數是一個陣列,其中包含要傳遞給函式的引數。apply() 方法會立即執行函式。


function addNumbers(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];

const result = addNumbers.apply(null, numbers);
console.log(result); // 輸出 6

在上面的例子中,我們定義了一個 addNumbers() 函式,它接受三個引數並返回它們的和。然後,我們定義了一個陣列 numbers,其中包含要傳遞給 addNumbers() 函式的引數。我們使用 apply() 方法將陣列作為引數傳遞給函式,並將 this 值設定為 null。最後,我們列印出 addNumbers() 函式的返回值。

call

call() 方法和 apply() 方法非常相似,但是它的引數不是陣列,而是一個一個傳遞的。call() 方法也可以用於改變函式的 this 值。

function greet(name) {
  console.log(`Hello, ${name}!`);
}

greet.call(null, 'Alice'); // 輸出 "Hello, Alice!"

在上面的例子中,我們定義了一個 greet() 函式,它接受一個引數 name,並在控制檯中輸出一條歡迎資訊。我們使用 call() 方法將 this 值設定為 null,並將要傳遞給函式的引數一個一個地傳遞。

bind

bind() 方法和 apply() 方法和 call() 方法有些不同。它不會立即執行函式,而是返回一個新的函式,這個函式的 this 值已經被改變了。我們可以在之後的任何時候呼叫這個函式。並且傳遞的部分引數也可在後面使用。

const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, my name is ${this.name}!`);
  }
};

const greetAlice = person.greet.bind(person);
greetAlice(); // 輸出 "Hello, my name is Alice!"

在上面的例子中,我們定義了一個 person 物件,其中包含一個 greet() 方法。然後,我們使用 bind() 方法建立一個新的函式 greetAlice,並將 person 物件作為 this 值繫結。最後,我們呼叫 greetAlice() 函式,它會輸出 "Hello, my name is Alice!"。

apply()、call() 和 bind() 的區別

apply()、call() 和 bind()三個方法之間的最大區別在於它們傳遞引數的方式以及它們是否是立即執行函式:

  • apply() 和 call() 方法立即執行函式並且傳遞引數的方式不同。
  • apply() 方法傳遞一個陣列作為引數,而 call() 方法一個個傳遞引數。
  • bind() 方法不會立即執行函式,而是返回一個新的函式,這個函式的 this 值已經被繫結了。

在使用這些方法時,我們需要理解它們之間的區別,並根據具體情況進行選擇。如果我們想要立即執行一個函式並傳遞一個陣列作為引數,那麼應該使用 apply() 方法。如果我們想要立即執行一個函式並逐個傳遞引數,那麼應該使用 call() 方法。如果我們想要建立一個新的函式並繫結 this 值,那麼應該使用 bind() 方法。

此外,在使用 apply()、call() 和 bind() 方法時,我們需要注意傳遞的 this 值和引數是否正確。如果我們不傳遞 this 值或傳遞一個錯誤的 this 值,那麼函式可能無法正確地執行。同樣,如果我們沒有正確地傳遞引數,那麼函式的結果也可能會出現錯誤。

內部機制

瞭解這些方法的內部機制可以幫助我們更好地理解它們的使用方式。在 JavaScript 中,函式是物件,因此每個函式都有一個 prototype 物件。當我們建立一個函式時,它會自動建立一個 prototype 物件,並將該物件的 constructor 屬性設定為函式本身。

apply()、call() 和 bind() 方法都是在 Function.prototype 上定義的(每個函式都是 Function 物件的例項),因此每個函式都可以使用它們。當我們呼叫 apply() 或 call() 方法時,JavaScript 引擎會將 this 值設定為傳遞給方法的第一個引數,並將要傳遞給函式的引數作為陣列或單個引數傳遞。當我們呼叫 bind() 方法時,它會返回一個新的函式,並將 this 值設定為繫結的值。

下面是 apply() 方法的一個簡單實現:

Function.prototype.myApply = function(thisArg, args) {
  // 首先判斷呼叫 myApply() 方法的物件是否為函式
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myApply called on non-function');
  }
  
  // 如果 thisArg 為 null 或 undefined,則將其設定為全域性物件
  if (thisArg === null || thisArg === undefined) {
    thisArg = window;
  }

  // 將函式設定為 thisArg 的方法,以便在 thisArg 上呼叫它
  thisArg.__myApply__ = this;

  // 執行函式並獲取結果
  const result = thisArg.__myApply__(...args);

  // 刪除在 thisArg 上設定的方法
  delete thisArg.__myApply__;

  // 返回函式的執行結果
  return result;
};

call() 方法的實現機制與 apply() 方法類似,只是在呼叫函式時將引數傳遞給函式的方式有所不同。call() 方法的實現如下:


Function.prototype.myCall = function(thisArg, ...args) {
  // ... 如上 只是引數需要用...rest 展開

};

bind() 方法的實現機制稍微有些不同。bind() 方法會建立一個新的函式,並將原函式的 this 值繫結到指定的物件上。當新函式被呼叫時,它會將繫結的 this 值傳遞給原函式,並將任何附加的引數一併傳遞。bind() 方法的實現如下:

Function.prototype.myBind = function(thisArg, ...args) {
  // 首先判斷呼叫 myBind() 方法的物件是否為函式
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myBind called on non-function');
  }

  // 儲存原函式的引用
  const originalFunc = this;

  // 返回一個新函式
  return function boundFunc(...newArgs) {
    // 將 this 值設定為 thisArg,並呼叫原函式
    return originalFunc.apply(thisArg, [...args, ...newArgs]);
    };
};

這裡,我們透過使用剩餘引數和展開語法來處理任意數量的傳遞引數,同時使用 apply() 方法將繫結的 this 值和所有引數傳遞給原函式。

需要注意的是,這裡的 bind() 方法是定義在 Function 的原型上的。這意味著,所有的函式都可以呼叫這個方法,並將其繫結到需要的物件上。

值得一提的是,bind() 方法在繫結 this 值的同時,還可以透過將附加引數傳遞給新函式來“柯里化”函式。也就是說,透過預先繫結部分引數,我們可以建立一個新函式,這個函式接受剩餘的引數,並在呼叫原函式時將這些引數一併傳遞。這個技巧在實際開發中經常用到,可以幫助我們簡化程式碼,並提高程式碼的可讀性。

以上是 apply()、call() 和 bind() 方法的手動實現方式,當然,JavaScript 中的這些方法已經預先實現好了,我們只需要使用即可

apply()、call() 和 bind() 方法的使用場景

1. 改變函式的 this 值

在 JavaScript 中,函式的 this 值預設指向全域性物件(在瀏覽器中通常為 window 物件)。如果我們想要在函式內部使用不同的 this 值,那麼可以使用 apply()、call() 或 bind() 方法來改變它。例如:

const obj = { name: 'Alice' };

function greet() {
  console.log(`Hello, ${this.name}!`);
}

greet.apply(obj); // "Hello, Alice!"
greet.call(obj); // "Hello, Alice!"
const newGreet = greet.bind(obj);
newGreet(); // "Hello, Alice!"

在上面的程式碼中,我們使用 apply()、call() 和 bind() 方法來將函式的 this 值設定為 obj 物件,並在函式內部使用它。

2. 函式的引數不確定

有時候我們無法確定函式的引數個數,或者我們想要將一個陣列作為引數傳遞給函式。在這種情況下,我們可以使用 apply() 方法來將陣列作為引數傳遞給函式。例如:

function sum(a, b, c) {
  return a + b + c;
}

const nums = [1, 2, 3];

const result = sum.apply(null, nums);

console.log(result); // 6

在上面的程式碼中,我們使用 apply() 方法將 nums 陣列作為引數傳遞給 sum() 函式,從而計算出它們的和。

3. 繼承

在 JavaScript 中,我們可以使用 call() 方法來實現繼承。前面講解物件時也用到過。例如:

function Animal(name) {
  this.name = name;
}

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

const myDog = new Dog('Fido', 'Golden Retriever');

console.log(myDog.name); // "Fido"
console.log(myDog.breed); // "Golden Retriever"

在上面的程式碼中,我們定義了 Animal 和 Dog 兩個建構函式,並使用 call() 方法將 Animal 的屬性和方法繼承到 Dog 中。

4. 部分應用函式

使用 bind() 方法可以實現部分應用函式,即將一個函式的部分引數固定下來,返回一個新的函式。例如:

function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);

console.log(double(3)); // 6
console.log(double(4)); // 8

在上面的程式碼中,我們使用 bind() 方法將 multiply() 函式的第一個引數固定為 2,從而建立了一個新的函式 double(),它的作用是將傳入的引數乘以 2。

最後

總之,apply()、call() 和 bind() 方法非常有用,並且在 JavaScript 開發中經常被使用。熟練掌握它們的用法可以幫助我們更好地編寫高質量的程式碼。

相關文章