搞懂Javascript的call、apply、bind差異

前言

透過這篇將在框架源碼(如Vue.js)常常會用到的手法call、apply、bind做一些觀念的釐清,幫助自己在看源碼的時候可以更清楚框架作者的撰寫邏輯,也紀錄之前常搞錯觀念的例子提醒自己。


了解call、apply、bind用法

  • call

    用法:函式名稱.call(綁定物件,參數,…)
    返回值:函式執行後的結果

  • apply

    用法:函式名稱.apply(綁定物件,[參數,…])
    返回值:函式執行後的結果

  • bind (這邊不討論bind傳其他參數用法)

    用法:函式名稱.bind(新的綁定物件)
    返回值:函式本身

call和apply為函式呼叫

先來看以下範例

1
2
3
4
5
6
7
function testFn(a, b, c) {
console.log(a, b, c)
}

testFn(1,2,3) // 1,2,3
testFn.call(null, 1, 2, 3) // 1,2,3
testFn.apply(null, [1, 2, 3]) // 1,2,3

上述結果都是印出1,2,3,testFn(1,2,3)為一般函式呼叫方式,應該都很熟悉。
上述call和apply不綁定任何物件,所以塞了一個null,然後填上參數名(要留意apply的用法的參數是要用陣列[]裝起來),並執行,可以證明call和apply的確就是執行函式,然後返為執行後的結果。

bind為綁定物件

常見範例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
constructor(initalName) {
this.Name = initalName;
}
sayHi() {
console.log(this.Name);
}
delaySayHi() {
setTimeout(function() {
console.log('...Hi, My name is ', this.Name)
}, 1000);
}
}
let person = new Person('Fred');
person.sayHi();
person.delaySayHi();

會發現印出結果是

會發現setTimeout裡面印出的this.name會是undefined,因為這個this是指向全域的Window物件,該如何解決呢?
我們這時候就能夠用bind,所以改為以下程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
constructor(initalName) {
this.Name = initalName;
}
sayHi() {
console.log(this.Name);
}
delaySayHi() {
setTimeout(function() {
console.log('...Hi, My name is ', this.Name)
}.bind(this), 1000);
}
}
let person = new Person('Fred');
person.sayHi();
person.delaySayHi();

印出結果是

看起來好像有點亂、難理解,但其實仔細看,setTimeout函式的第一個參數就是函式,所以上述我們有介紹過bind用法,所以就是直接針對以下的函式:

1
2
3
function() {
console.log('...Hi, My name is ', this.Name)
}

上述這個函式後面加上 .bind(this),就完成bind(這個this就是指向這個Person物件,所以就能抓到Fred的值。相關觀念可看這篇文章)
(當然更直覺作法就是用ES6出現的箭頭函式,就能更直觀的解決,但這個不是我們今天要討論的重點,所以就先略過。)

bind用法觀念釐清

接下來的一個範例,是我以前在學習bind時發生的錯誤觀念產生的寫法

1
2
3
4
5
6
7
const aaa = {
x: 123,
printX: function() {
return this.x;
}.bind(this), //這是錯誤作法,會印出undefined
}
console.log(aaa.printX())

本來以為多了一個bind(this)也應該沒事,但這個this其實是指向全域的Window物件,所以結果是印出undefined
這個也幫助我釐清另一個觀念,物件裡面的this必須是包在傳統函式function(){}裡面的this才是指向物件本身。
所以如果要印出this.x的值,把.bind(this)刪掉即可

1
2
3
4
5
6
7
const aaa = {
x: 123,
printX: function() {
return this.x;
}
}
console.log(aaa.printX())

就能印出123的值了

這時候,我們再將上述範例做一點延伸

綜合call、apply、bind綁定物件

1
2
3
4
5
6
7
8
9
const aaa = {
x: 123,
printX: function() {
return this.x;
}
}
const showX = aaa.printX;
console.log(aaa.printX()); //123
console.log(showX()); //undefined

aaa.printX 指派給showX 函式,然後用一般函式的方式呼叫showX,因為這個函式的this就是指向全域的Window物件,所以第9行是印出undefined
這時候這篇文章的三大主角都能處理這個問題,程式碼如下

1
2
3
console.log(showX.call(aaa)); //123
console.log(showX.apply(aaa)); //123
console.log(showX.bind(aaa)()); //123

三個方式都印出我們要的結果,我們綁定了aaa物件並執行它。
要特別留意,bind後要再多一個括號(),代表要去執行該函式。


小結

  1. call和apply都是執行函式,所以會return函式的結果;但bind就不同了,它只是單純綁定物件,return函式本身(也就是未執行)
  2. 如果call和apply不綁定物件就填上null即可,就是會跟一般函式的執行結果一樣。(但我們一般不太會去綁null,有點脫褲子放屁的感覺www)
  3. 這三個用法常常會跟this扯上關係,是用來改變原函式this的指向。