【譯】JavaScript 原型的深入指南

前端小智發表於2019-04-24

譯者:前端小智

原文:tylermcginnis.com/beginners-g…

不學會怎麼處理物件,你在 JavaScript 道路就就走不了多遠。它們幾乎是 JavaScript 程式語言每個方面的基礎。事實上,學習如何建立物件可能是你剛開始學習的第一件事。

物件是鍵/值對。建立物件的最常用方法是使用花括號{},並使用表示法向物件新增屬性和方法。

let animal = {}
animal.name = 'Leo'
animal.energy = 10

animal.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

animal.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

animal.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}
複製程式碼

現在,在我們的應用程式中,我們需要建立多個 animal。 當然,下一步是將邏輯封裝,當我們需要建立新 animal 時,只需呼叫函式即可,我們將這種模式稱為函式的例項化(unctional Instantiation),我們將函式本身稱為“建構函式”,因為它負責“構造”一個​​新物件。

函式的例項化

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy

  animal.eat = function (amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }

  animal.sleep = function (length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }

  animal.play = function (length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)
複製程式碼

現在,無論何時我們想要建立一個新 animal(或者更廣泛地說,建立一個新的“例項”),我們所要做的就是呼叫我們的 Animal 函式,並傳入引數:nameenergy 。這很有用,而且非常簡單。但是,你能說這種模式的哪些缺點嗎?

最大的和我們試圖解決的問題與函式裡面的三個方法有關 - eatsleepplay。 這些方法中的每一種都不僅是動態的,而且它們也是完全通用的。這意味著,我們沒有理由像現在一樣,在創造新animal的時候重新建立這些方法。我們只是在浪費記憶體,讓每一個新建的物件都比實際需要的還大。

你能想到一個解決方案嗎? 如果不是在每次建立新動物時重新建立這些方法,我們將它們移動到自己的物件然後我們可以讓每個動物引用該物件,該怎麼辦? 我們可以將此模式稱為函式例項化與共享方法

函式例項化與共享方法

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy
  animal.eat = animalMethods.eat
  animal.sleep = animalMethods.sleep
  animal.play = animalMethods.play

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)
複製程式碼

通過將共享方法移動到它們自己的物件並在 Animal 函式中引用該物件,我們現在已經解決了記憶體浪費和新物件體積過大的問題。

Object.create

讓我們再次使用 Object.create 改進我們的例子。 簡單地說,Object.create 允許你建立一個物件,該物件將在失敗的查詢中委託給另一個物件。 換句話說,Object.create 允許你建立一個物件,只要該物件上的屬性查詢失敗,它就可以查詢另一個物件以檢視該另一個物件是否具有該屬性。 我們來看一些程式碼:

const parent = {
  name: 'Stacey',
  age: 35,
  heritage: 'Irish'
}

const child = Object.create(parent)
child.name = 'Ryan'
child.age = 7

console.log(child.name) // Ryan
console.log(child.age) // 7
console.log(child.heritage) // Irish
複製程式碼

因此,在上面的示例中,由於 child 是用 object.create(parent) 建立的,所以每當child 物件上的屬性查詢失敗時,JavaScript 就會將該查詢委託給 parent 物件。這意味著即使 child 沒有屬性 heritage ,當你列印 child.heritage 時,它會從 parent 物件中找到對應 heritage 並列印出來。

現在如何使用 Object.create 來簡化之前的 Animal程式碼? 好吧,我們可以使用Object.create 來委託給animalMethods物件,而不是像我們現在一樣逐一向 animal 新增所有共享方法。 為了B 格一點,就叫做 使用共享方法 和 Object.create 的函式例項化

使用共享方法 和 Object.create 的函式例項化

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = Object.create(animalMethods)
  animal.name = name
  animal.energy = energy

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)
複製程式碼

所以現在當我們呼叫 leo.eat 時,JavaScript 將在 leo 物件上查詢 eat 方法,因為 leo 中沒有 eat 方法,所以查詢將失敗,由於 Object.create,它將委託給animalMethods物件,所以會從 animalMethods 物件上找到 eat 方法。

到現在為止還挺好。儘管如此,我們仍然可以做出一些改進。為了跨例項共享方法,必須管理一個單獨的物件(animalMethods)似乎有點“傻哈”。我們希望這在語言本身中實現的一個常見特,所以就需要引出下一個屬性 - prototype

那麼究竟 JavaScript 中的 prototype 是什麼? 好吧,簡單地說,JavaScript 中的每個函式都有一個引用物件的prototype屬性。

function doThing () {}
console.log(doThing.prototype) // {}
複製程式碼

如果不是建立一個單獨的物件來管理我們的方法(如上例中 animalMethods),我們只是將每個方法放在 Animal 函式的 prototype 上,該怎麼辦? 然後我們所要做的就是不使用Object.create 委託給animalMethods,我們可以用它來委託Animal.prototype。 我們將這種模式稱為 原型例項化。

原型(prototype)例項化

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype)
  animal.name = name
  animal.energy = energy

  return animal
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)
複製程式碼

同樣,prototype 只是 JavaScript 中的每個函式都具有的一個屬性,正如我們前面看到的,它允許我們跨函式的所有例項共享方法。我們所有的功能仍然是相同的,但是現在我們不必為所有的方法管理一個單獨的物件,我們只需要使用 Animal 函式本身內建的另一個物件Animal.prototype

更進一步

現在我們知道三個點:

  1. 如何建立建構函式。

  2. 如何向建構函式的原型新增方法。

  3. 如何使用 Object.create 將失敗的查詢委託給函式的原型。

這三個點對於任何程式語言來說都是非常基礎的。JavaScript 真的有那麼糟糕,以至於沒有更簡單的方法來完成同樣的事情嗎?正如你可能已經猜到的那樣,現在已經有了,它是通過使用new關鍵字來實現的。

回顧一下我們的 Animal 建構函式,最重要的兩個部分是建立物件並返回它。 如果不使用Object.create建立物件,我們將無法在失敗的查詢上委託函式的原型。 如果沒有return語句,我們將永遠不會返回建立的物件。

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype) // 1 
  animal.name = name
  animal.energy = energy

  return animal   // 2
}
複製程式碼

關於 new,有一件很酷的事情——當你使用new關鍵字呼叫一個函式時,以下編號為12兩行程式碼將隱式地(在底層)為你完成,所建立的物件被稱為this

使用註釋來顯示底層發生了什麼,並假設用new關鍵字呼叫了Animal建構函式,可以這樣重寫它。

function Animal (name, energy) {
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy

  // return this
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
複製程式碼

正常如下:

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
複製程式碼

再次說明,之所以這樣做,並且這個物件是為我們建立的,是因為我們用new關鍵字呼叫了建構函式。如果在呼叫函式時省略new,則永遠不會建立該物件,也不會隱式地返回該物件。我們可以在下面的例子中看到這個問題。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = Animal('Leo', 7)
console.log(leo) // undefined
複製程式碼

這種模式稱為 偽類例項化

對於那些不熟悉的人,允許你為物件建立藍圖。 然後,每當你建立該類的例項時,你可以訪問這個物件中定義的屬性和方法。

聽起來有點熟? 這基本上就是我們對上面的 Animal 建構函式所做的。 但是,我們只使用常規的舊 JavaScript 函式來重新建立相同的功能,而不是使用class關鍵字。 當然,它需要一些額外的工作以及瞭解一些 JavaScript “底層” 發生的事情,但結果是一樣的。

這是個好訊息。 JavaScript 不是一種死語言。 TC-39委員會不斷改進和補充。 這意味著即使JavaScript的初始版本不支援類,也沒有理由將它們新增到官方規範中。 事實上,這正是TC-39委員會所做的。 2015 年,釋出了EcmaScript(官方JavaScript規範)6,支援類和class關鍵字。 讓我們看看上面的Animal建構函式如何使用新的類語法。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
複製程式碼

這個相對前面的例子,是相對簡單明瞭的。

因此,如果這是建立類的新方法,為什麼我們花了這麼多時間來複習舊的方式呢? 原因是因為新方法(使用class關鍵字)主要只是我們稱之為偽類例項化模式現有方式的“語法糖”。 為了完全理解 ES6 類的便捷語法,首先必須理解偽類例項化模式

至此,我們已經介紹了 JavaScript 原型的基本原理。這篇文章的其餘部分將致力於理解與之相關的其他好話題。在另一篇文章中,我們將研究如何利用這些基本原理,並使用它們來理解JavaScript中的繼承是如何工作的。

陣列方法

我們在上面深入討論瞭如何在一個類的例項之間共享方法,你應該將這些方法放在類(或函式)原型上。 如果我們檢視Array類,我們可以看到相同的模式。

onst friends = []
複製程式碼

以為是代替使用 new Array() 的一個語法糖。

const friendsWithSugar = []

const friendsWithoutSugar = new Array()
複製程式碼

你可能從未想過的一件事是,陣列的每個例項如何具有所有內建方法 (splice, slice, pop 等)?

正如你現在所知,這是因為這些方法存在於 Array.prototype 上,當你建立新的Array例項時,你使用new關鍵字在失敗的查詢中將該委託設定為 Array.prototype

我們可以列印 Array.prototype 來檢視有哪些方法:

console.log(Array.prototype)

/*
  concat: ƒn concat()
  constructor: ƒn Array()
  copyWithin: ƒn copyWithin()
  entries: ƒn entries()
  every: ƒn every()
  fill: ƒn fill()
  filter: ƒn filter()
  find: ƒn find()
  findIndex: ƒn findIndex()
  forEach: ƒn forEach()
  includes: ƒn includes()
  indexOf: ƒn indexOf()
  join: ƒn join()
  keys: ƒn keys()
  lastIndexOf: ƒn lastIndexOf()
  length: 0n
  map: ƒn map()
  pop: ƒn pop()
  push: ƒn push()
  reduce: ƒn reduce()
  reduceRight: ƒn reduceRight()
  reverse: ƒn reverse()
  shift: ƒn shift()
  slice: ƒn slice()
  some: ƒn some()
  sort: ƒn sort()
  splice: ƒn splice()
  toLocaleString: ƒn toLocaleString()
  toString: ƒn toString()
  unshift: ƒn unshift()
  values: ƒn values()
*/
複製程式碼

物件也存在完全相同的邏輯。所有的物件將在失敗的查詢後委託給 Object.prototype,這就是所有物件都有 toStringhasOwnProperty 等方法的原因

靜態方法

到目前為止,我們已經討論了為什麼以及如何在類的例項之間共享方法。但是,如果我們有一個對類很重要的方法,但是不需要在例項之間共享該方法怎麼辦?例如,如果我們有一個函式,它接收一系列 Animal 例項,並確定下一步需要餵養哪一個呢?我們這個方法叫做 nextToEat

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}
複製程式碼

因為我們不希望在所有例項之間共享 nextToEat,所以在 Animal.prototype上使用nextToEat 是沒有意義的。 相反,我們可以將其視為輔助方法。

所以如果nextToEat不應該存在於Animal.prototype中,我們應該把它放在哪裡? 顯而易見的答案是我們可以將nextToEat放在與我們的Animal類相同的範圍內,然後像我們通常那樣在需要時引用它。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(nextToEat([leo, snoop])) // Leo
複製程式碼

這是可行的,但是還有一個更好的方法。

只要有一個特定於類本身的方法,但不需要在該類的例項之間共享,就可以將其定義為類的靜態屬性

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
  static nextToEat(animals) {
    const sortedByLeastEnergy = animals.sort((a,b) => {
      return a.energy - b.energy
    })

    return sortedByLeastEnergy[0].name
  }
}
複製程式碼

現在,因為我們在類上新增了nextToEat作為靜態屬性,所以它存在於Animal類本身(而不是它的原型)上,並且可以使用Animal.nextToEat進行呼叫 。

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(Animal.nextToEat([leo, snoop])) // Leo
複製程式碼

因為我們在這篇文章中都遵循了類似的模式,讓我們來看看如何使用 ES5 完成同樣的事情。 在上面的例子中,我們看到了如何使用 static 關鍵字將方法直接放在類本身上。 使用 ES5,同樣的模式就像手動將方法新增到函式物件一樣簡單。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

Animal.nextToEat = function (nextToEat) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(Animal.nextToEat([leo, snoop])) // Leo
複製程式碼

獲取物件的原型

無論您使用哪種模式建立物件,都可以使用Object.getPrototypeOf方法完成獲取該物件的原型。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)
const proto  = Object.getPrototypeOf(leo)

console.log(proto )
// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}

proto === Animal.prototype // true
複製程式碼

上面的程式碼有兩個重要的要點。

首先,你將注意到 proto 是一個具有 4 個方法的物件,constructoreatsleepplay。這是有意義的。我們使用getPrototypeOf傳遞例項,leo取回例項原型,這是我們所有方法的所在。

這也告訴了我們關於 prototype 的另一件事,我們還沒有討論過。預設情況下,prototype物件將具有一個 constructor 屬性,該屬性指向初始函式或建立例項的類。這也意味著因為 JavaScript 預設在原型上放置建構函式屬性,所以任何例項都可以通過。

第二個重要的點是Object.getPrototypeOf(leo) === Animal.prototype。 這也是有道理的。 Animal 建構函式有一個prototype屬性,我們可以在所有例項之間共享方法,getPrototypeOf 允許我們檢視例項本身的原型。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = new Animal('Leo', 7)
console.log(leo.constructor) // Logs the constructor function
複製程式碼

為了配合我們之前使用 Object.create 所討論的內容,其工作原理是因為任何Animal例項都會在失敗的查詢中委託給Animal.prototype。 因此,當你嘗試訪問leo.constructor時,leo沒有 constructor 屬性,因此它會將該查詢委託給 Animal.prototype,而Animal.prototype 確實具有建構函式屬性。

你之前可能看過使用 __proto__ 用於獲取例項的原型,這是過去的遺物。 相反,如上所述使用 Object.getPrototypeOf(instance)

判斷原型上是否包含某個屬性

在某些情況下,你需要知道屬性是否存在於例項本身上,還是存在於物件委託的原型上。 我們可以通過迴圈列印我們建立的leo物件來看到這一點。 使用for in 迴圈方式如下:

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)

for(let key in leo) {
  console.log(`Key: ${key}. Value: ${leo[key]}`)
}
複製程式碼

我所期望的列印結果可能如下:

Key: name. Value: Leo
Key: energy. Value: 7
複製程式碼

然而,如果你執行程式碼,看到的是這樣的-

Key: name. Value: Leo
Key: energy. Value: 7
Key: eat. Value: function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}
Key: sleep. Value: function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}
Key: play. Value: function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}
複製程式碼

這是為什麼? 對於for in迴圈來說,迴圈將遍歷物件本身以及它所委託的原型的所有可列舉屬性。 因為預設情況下,你新增到函式原型的任何屬性都是可列舉的,我們不僅會看到nameenergy,還會看到原型上的所有方法 -eatsleepplay

要解決這個問題,我們需要指定所有原型方法都是不可列舉的,或者只列印屬性位於 leo 物件本身而不是leo委託給失敗查詢的原型。 這是 hasOwnProperty 可以幫助我們的地方。

...

const leo = new Animal('Leo', 7)

for(let key in leo) {
  if (leo.hasOwnProperty(key)) {
    console.log(`Key: ${key}. Value: ${leo[key]}`)
  }
}
複製程式碼

現在我們看到的只是leo物件本身的屬性,而不是leo委託的原型。

Key: name. Value: Leo
Key: energy. Value: 7
複製程式碼

果你仍然對 hasOwnProperty 感到困惑,這裡有一些程式碼可以幫你更好的理清它。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)

leo.hasOwnProperty('name') // true
leo.hasOwnProperty('energy') // true
leo.hasOwnProperty('eat') // false
leo.hasOwnProperty('sleep') // false
leo.hasOwnProperty('play') // false
複製程式碼

檢查物件是否是類的例項

有時你想知道物件是否是特定類的例項。 為此,你可以使用instanceof運算子。 用例非常簡單,但如果你以前從未見過它,實際的語法有點奇怪。 它的工作方式如下

object instanceof Class
複製程式碼

如果 objectClass的例項,則上面的語句將返回 true,否則返回 false。 回到我們的 Animal 示例,我們有類似的東西:

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

function User () {}

const leo = new Animal('Leo', 7)

leo instanceof Animal // true
leo instanceof User // false
複製程式碼

instanceof 的工作方式是檢查物件原型鏈中是否存在 constructor.prototype。 在上面的例子中,leo instanceof Animal 為 true,因為 Object.getPrototypeOf(leo) === Animal.prototype。 另外,leo instanceof User 為 false,因為Object.getPrototypeOf(leo) !== User.prototype

建立新的不可知的建構函式

你能找出下面程式碼中的錯誤嗎

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = Animal('Leo', 7)
複製程式碼

即使是經驗豐富的 JavaScript 開發人員有時也會被上面的例子絆倒。因為我們使用的是前面學過的偽類例項模式,所以在呼叫Animal建構函式時,需要確保使用new關鍵字呼叫它。如果我們不這樣做,那麼 this 關鍵字就不會被建立,它也不會隱式地返回。

作為複習,註釋掉的行是在函式上使用new關鍵字時背後發生的事情。

function Animal (name, energy) {
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy

  // return this
}
複製程式碼

讓其他開發人員記住,這似乎是一個非常重要的細節。 假設我們正在與其他開發人員合作,我們是否有辦法確保始終使用new關鍵字呼叫我們的Animal建構函式? 事實證明,可以通過使用我們之前學到的instanceof運算子來實現的。

如果使用new關鍵字呼叫建構函式,那麼建構函式體的內部 this 將是建構函式本身的例項。

function Aniam (name, energy) {
  if (this instanceof Animal === false) {
     console.warn('Forgot to call Animal with the new keyword')
  }

  this.name = name
  this.energy = energy
}
複製程式碼

現在,如果我們重新呼叫函式,但是這次使用 new 的關鍵字,而不是僅僅向函式的呼叫者列印一個警告呢?

function Animal (name, energy) {
  if (this instanceof Animal === false) {
    return new Animal(name, energy)
  }

  this.name = name
  this.energy = energy
}
複製程式碼

現在,不管是否使用new關鍵字呼叫Animal,它仍然可以正常工作。

重寫 Object.create

在這篇文章中,我們非常依賴於Object.create來建立委託給建構函式原型的物件。 此時,你應該知道如何在程式碼中使用Object.create,但你可能沒有想到的一件事是Object.create實際上是如何工作的。 為了讓你真正瞭解Object.create的工作原理,我們將自己重新建立它。 首先,我們對Object.create的工作原理了解多少?

  1. 它接受一個物件的引數。

  2. 它建立一個物件,在查詢失敗時委託給引數物件

  3. 它返回新建立的物件。

    Object.create = function (objToDelegateTo) {

    }

現在,我們需要建立一個物件,該物件將在失敗的查詢中委託給引數物件。 這個有點棘手。 為此,我們將使用 new 關鍵字相關的知識。

首先,在 Object.create 主體內部建立一個空函式。 然後,將空函式的 prototype 設定為等於傳入引數物件。 然後,返回使用new關鍵字呼叫我們的空函式。

Object.create = function (objToDelegateTo) {
  function Fn(){}
  Fn.prototype = objToDelegateTo
  return new Fn()
}
複製程式碼

當我們在上面的程式碼中建立一個新函式Fn時,它帶有一個prototype屬性。 當我們使用new關鍵字呼叫它時,我們知道我們將得到的是一個將在失敗的查詢中委託給函式原型的物件。

如果我們覆蓋函式的原型,那麼我們可以決定在失敗的查詢中委託哪個物件。 所以在上面的例子中,我們用呼叫Object.create時傳入的物件覆蓋Fn的原型,我們稱之為objToDelegateTo

請注意,我們只支援 Object.create 的單個引數。 官方實現還支援第二個可選引數,該引數允許你向建立的物件新增更多屬性。

箭頭函式

箭頭函式沒有自己的this關鍵字。 因此,箭頭函式不能是建構函式,如果你嘗試使用new關鍵字呼叫箭頭函式,它將引發錯誤。

const Animal = () => {}

const leo = new Animal() // Error: Animal is not a constructor
複製程式碼

另外,因為我們在上面說明了偽類例項模式不能與箭頭函式一起使用,所以箭頭函式也沒有原型屬性。

const Animal = () => {}
console.log(Animal.prototype) // undefined
複製程式碼

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

你的點贊是我持續分享好東西的動力,歡迎點贊!

歡迎加入前端大家庭,裡面會經常分享一些技術資源。

clipboard.png

相關文章