從一行等式理解JS當中的call, apply和bind

Jrain發表於2019-01-31

寫於 2018.04.04

關於JS當中的call,apply和bind,相信大家和我一樣,已經看過了無數篇相關的文章,都有自己的理解。所以這篇文章並非什麼科普類的文章,僅僅是把我自己的理解記錄下來。

我的學習習慣,是喜歡把各種看似孤立的知識點串聯起來,綜合理解並運用,通過最簡單最直觀的思路把它理解透。所以,這篇文章將通過一段非常簡潔的等式,把JS當中一個相對較難的知識點,call,apply和bind給串聯起來:

cat.call(dog, a, b) = cat.apply(dog, [a, b]) = (cat.bind(dog, a, b))() = dog.cat(a, b)
複製程式碼

要理解JS當中的這三個關鍵字,首先得弄清楚它們是用來幹嘛的。複雜些來說,可以引用MDN文件的原文:

可以讓call()中的物件呼叫當前物件所擁有的function。你可以使用call()來實現繼承:寫一個方法,然後讓另外一個新的物件來繼承它(而不是在新物件中再寫一次這個方法)。

簡單些來說,可以引用大家都看過的一句話:

為了動態改變某個函式執行時的上下文(context)。

又或者是

為了改變函式體內部this的指向

上面這些解釋都很正確,說得一點問題都沒有,但是裡面卻又引入了繼承上下文this這些額外的知識點。如果我只想用最直觀的辦法去理解這三個關鍵字的作用,也許可以這麼去理解:

定義一個貓物件:

class Cat {
  constructor (name) {
    this.name = name
  }

  catchMouse(name1, name2) {
    console.log(`${this.name} caught 2 mouse! They call ${name1} and ${name2}.`)
  }
}
複製程式碼

這個貓物件擁有一個抓老鼠的技能catchMouse()

然後類似的,定義一個狗物件:

class Dog {
  constructor (name) {
    this.name = name
  }

  biteCriminals(name1, name2) {
    console.log(`${this.name} bite 2 criminals! Their name is ${name1} and ${name2}.`)
  }
}
複製程式碼

這個狗物件能夠咬壞人biteCriminal()

接下來,我們例項化兩個物件,分別得到一隻叫“Kitty”的貓和叫“Doggy”的狗:

const kitty = new Cat('Kitty')
const doggy = new Dog('Doggy')
複製程式碼

首先讓它們彼此發揮自己的技能:

kitty.catchMouse('Mickey', 'Minnie')
// Kitty caught mouse! They call Mickey and Minnie.

doggy.biteCriminal('Tom', 'Jerry')
// Doggy bite a criminal! Their name is Tom and Jerry.
複製程式碼

現在,我們希望賦予Doggy抓老鼠的能力,如果不使用這三個關鍵字,應該怎麼做呢?

方案A:修改Dog物件,直接為其定義一個和Cat相同的抓老鼠技能。

方案B:讓Doggy吃掉Kitty,直接消化吸收Kitty的所有能力。

其實方案A和方案B的解決辦法是類似的,也是需要修改Dog物件,不過方案B會更簡單粗暴一點:

class Dog {
  constructor (name, kitty) {
    this.name = name
    this.catchMouse = kitty.catchMouse
  }

  biteCriminals(name1, name2) {
    console.log(`${this.name} bite 2 criminals! Their name is ${name1} and ${name2}.`)
  }
}

const kitty = new Cat('Kitty')
const doggy = new Dog('Doggy', kitty)

doggy.catchMouse('Mickey', 'Minnie')
// Doggy caught 2 mouse! They call Mickey and Minnie.
複製程式碼

上面這種方法實在是太不優雅,往往很多時候在定義Dog對像的時候根本就沒有打算過要為它新增抓老鼠的方法。那麼有沒有一種辦法能夠在不修改Dog物件內容的前提下,讓Doggy例項也能夠擁有抓老鼠的辦法呢?答案就是使用call,apply或者bind關鍵字:

kitty.catchMouse.call(doggy, 'Mickey', 'Minnie')

kitty.catchMouse.apply(doggy, ['Mickey', 'Minnie'])

const doggyCatchMouse = kitty.catchMouse.bind(doggy, 'Mickey', 'Minnie')
doggyCatchMouse()

// Doggy caught 2 mouse! They call Mickey and Minnie.
// Doggy caught 2 mouse! They call Mickey and Minnie.
// Doggy caught 2 mouse! They call Mickey and Minnie.
複製程式碼

反過來,讓Kitty擁有咬壞人的能力,也可以通過這種辦法實現,讀者可以自行嘗試。

看到這裡,相信讀者已經能夠明白call,apply和bind的區別及作用,反過來再檢視各自的概念,應該也能夠更容易理解。

回到文章開頭的等式:

cat.call(dog, a, b) = cat.apply(dog, [a, b]) = (cat.bind(dog, a, b))() = dog.cat(a, b)
複製程式碼

這裡的“等號”其實並不嚴謹,因為三個關鍵字的區別及背後的原理肯定不是區區一個等號就能夠概括的,但是對於概念的理解以及實際情況下的運用來說,這條等式未必不是一個好的思路。

相關文章