1. 程式人生 > >JavaScript原型初學者指南

JavaScript原型初學者指南

前言

如果不好好的學習物件,你就無法在JavaScript中獲得很大的成就。它們幾乎是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 }

如上程式碼,在我們的應用程式中,我們需要建立多個動物。當然,下一步是將邏輯封裝在我們可以在需要建立新動物時呼叫的函式內部。我們將這種模式稱為Functional 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)

現在,每當我們想要創造一種新動物(或者更廣泛地說是一種新的“例項”)時,我們所要做的就是呼叫我們的動物功能,將動物的名字和能量水平傳遞給它。這非常有效,而且非常簡單。但是,你能發現這種模式的弱點嗎?最大的和我們試圖解決的問題與三種方法有關 - 吃飯,睡覺和玩耍。這些方法中的每一種都不僅是動態的,而且它們也是完全通用的。這意味著沒有理由重新建立這些方法,正如我們在建立新動物時所做的那樣。你能想到一個解決方案嗎?如果不是每次建立新動物時重新建立這些方法,我們將它們移動到自己的物件然後我們可以讓每個動物引用該物件,該怎麼辦?我們可以將這種模式稱為功能例項化與共享方法。

使用共享方法的功能例項化

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)建立的,所以每當在子級上查詢失敗的屬性時,JavaScript都會將該查詢委託給父物件。這意味著即使孩子沒有遺產,父母也會在記錄時這樣做。這樣你就會得到父母的遺產(屬性值的傳遞)。

現在在我們的工具中使用Object.create,我們如何使用它來簡化之前的Animal程式碼?好吧,我們可以使用Object.create委託給animalMethods物件,而不是像我們現在一樣逐個將所有共享方法新增到動物中。聽起來很聰明,讓我們將這個稱為功能例項化與共享方法用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方法。那個查詢將失敗,Object.create,它將委託給animalMethods物件。

到現在為止還挺好。儘管如此,我們仍然可以做出一些改進。為了跨例項共享方法,必須管理一個單獨的物件(animalMethods)似乎有點“hacky”。這似乎是您希望在語言本身中實現的常見功能。這就是你在這裡的全部原因 - prototype。

那麼究竟什麼是JavaScript的原型?好吧,簡單地說,JavaScript中的每個函式都有一個引用物件的prototype屬性。對嗎?親自測試一下。

function doThing () {}
console.log(doThing.prototype) // {}

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

原型例項化


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)

同樣,原型只是JavaScript中每個函式都具有的屬性,並且如上所述,它允許我們在函式的所有例項之間共享方法。我們所有的功能仍然相同,但現在我們不必為所有方法管理一個單獨的物件,我們可以使用另一個內置於Animal函式本身的物件Animal.prototype。

在這一點上,我們知道三件事:

如何建立建構函式。
如何將方法新增到建構函式的原型中。
如何使用Object.create將失敗的查詢委託給函式的原型。(繼承)
這三個任務似乎是任何程式語言的基礎。JavaScript是否真的那麼糟糕,沒有更簡單“內建”的方式來完成同樣的事情?然而並不是的,它是通過使用new關鍵字來完成的。

我們採取的緩慢,有條理的方法有什麼好處,你現在可以深入瞭解JavaScript中新關鍵字的內容。

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

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

  return animal
}

這是關於new的一個很酷的事情 - 當你使用new關鍵字呼叫一個函式時,這兩行是隱式完成的(JavaScript引擎),並且建立的物件稱為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

此模式的名稱是Pseudoclassical Instantiation(原型例項化)。

如果JavaScript不是您的第一種程式語言,您可能會有點不安。

對於那些不熟悉的人,Class允許您為物件建立藍圖。然後,無論何時建立該類的例項,都會獲得一個具有藍圖中定義的屬性和方法的物件。

聽起來有點熟?這基本上就是我們對上面的Animal建構函式所做的。但是,我們只使用常規的舊JavaScript函式來重新建立相同的功能,而不是使用class關鍵字。當然,它需要一些額外的工作以及一些關於JavaScript引擎執行的知識,但結果是一樣的。

這是個好訊息。JavaScript不是一種死語言。它正在不斷得到改進,並由TC-39委員會新增。事實上,2015年,釋出了EcmaScript(官方JavaScript規範)6(ES6),支援Classes和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類,我們可以看到相同的模式。從歷史上看,您可能已經建立了這樣的陣列

const friends = []

事實證明,建立一個新的Array類其實也是一個語法糖。

const friendsWithSugar = []

const friendsWithoutSugar = new Array()

您可能從未想過的一件事是陣列的每個例項中的內建方法是從何而來的(splice, slice, pop, etc)?

正如您現在所知,這是因為這些方法存在於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,這就是所有物件都有toString和hasOwnProperty等方法的原因。

靜態方法

到目前為止,我們已經介紹了為什麼以及如何在類的例項之間共享方法。但是,如果我們有一個對Class很重要但不需要跨例項共享的方法呢?例如,如果我們有一個函式接受一個Animal例項陣列並確定下一個需要接收哪一個呢?我們將其稱為nextToEat
function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

因為我們不希望在所有例項之間共享它,所以在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作為靜態屬性(static),所以它存在於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)