英文原文 | https://tylermcginnis.com/beginners-guide-to-javascript-prototype/
引言:如果不處理對象,您就無法在 JavaScript 方面取得很大進展。它們幾乎是 JavaScript 程式語言的所有方面的基礎。在這篇文章中,您將了解用於實例化新對象的各種模式,在此過程中,您將逐步深入了解 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(函數實例化),並將函數本身稱為 constructor function(構造函數) ,因為它負責「構造」一個新對象。
Functional Instantiation (函數實例化)JavaScript 代碼:
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 函數,將動物的 name 和energy 傳遞給這個函數。這非常有效,而且非常簡單。但是,你有發現這種模式的不足之處嗎?我們要嘗試解決的最大的問題與三種方法有關 – eat,sleep 和 play。這些方法中的每一種都不僅是動態的,而且它們也是完全通用的。這意味著沒有理由重新創建這些方法,正如我們在創建新動物時所做的那樣。
我們只是在浪費內存,讓每個動物物體都比它需要的更大。你能想到一個解決方案嗎?
如果我們每次創建一個新動物時不需要重新創建這些方法,而是將它們移動到它們自己的對象上,那麼我們就可以讓每個動物引用那個對象了?我們可以把這種模式稱為 Functional Instantiation with Shared Methods(共享方法的函數實例化) 。描述起來有點囉嗦 🤷 。
Functional Instantiation with Shared Methods (共享方法的函數實例化)JavaScript 代碼:
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 允許您創建一個對象,只要該對象上的屬性查找失敗,它就可以查詢另一個對象,以查看另一個對象中是否具有該屬性。 說清楚需要很多文字, 我們來看一些代碼。
JavaScript 代碼:
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 屬性,即 Irish。
現在,通過使用 Object.create ,我們該如何使用它來簡化之前的 Animal 代碼呢?好吧,我們可以使用 Object.create 委託給animalMethods 對象,而不是像我們之前一樣逐個將所有共享方法添加到 Animal 中。 為了聽起來很智能,讓我們稱之為 Functional Instantiation with Shared Methods and Object.create(使用共享方法和Object.create進行函數實例化) 。
Functional Instantiation with Shared Methods and Object.create (使用共享方法和Object.create進行函數實例化)JavaScript 代碼:
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 對象,然後在這裡將找到 eat 方法。
到現在為止還挺好的。 儘管如此,我們仍然可以做出一些改進。 為了跨實例共享方法,必須管理一個單獨的對象(animalMethods)似乎有點「hacky」。 這似乎是您希望在語言本身中實現的常見功能。 事實證明,這就是你看這篇文章的原因 – prototype(原型) 。
那麼究竟什麼是 JavaScript 的 prototype(原型)呢? 簡單地說,JavaScript 中的每個函數都有一個引用對象的 prototype 屬性。 我們來親自測試一下。
JavaScript 代碼:
function doThing () {}
console.log(doThing.prototype) // {}
如果不是創建一個單獨的對象(比如我們正在使用的 animalMethods )來管理我們的方法,也就是我們只是將每個方法放在 Animal 函數的 prototype(原型) 對象上,該怎麼辦呢?我們所要做的就是不使用 Object.create 委託給 animalMethods,我們可以用使用來委託Animal.prototype。 我們將這種模式稱為 Prototypal Instantiation(原型實例化)。
Prototypal Instantiation (原型實例化)JavaScript 代碼:
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 中的 new 關鍵字在幕後的作用。
回顧一下我們的 Animal 構造函數,最重要的兩個部分是創建對象並返回它。 如果不使用 Object.create創建對象,我們將無法在查找失敗時委託給函數的原型。 如果沒有 return 語句,我們將永遠不會返回創建的對象。
JavaScript 代碼:
function Animal (name, energy) {
let animal = Object.create(Animal.prototype)
animal.name = name
animal.energy = energy
return animal
}
new 有一個很酷的地方——當您使用 new 關鍵字調用函數時,注釋掉的這兩行代碼是隱式(引擎)完成的,創建的對象稱為 this。
使用注釋來顯示在幕後發生的事情並假設使用 new 關鍵字調用 Animal 構造函數,可以將其重寫為這樣:
JavaScript 代碼:
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)
去掉注釋後:
JavaScript 代碼:
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)
同樣,這樣做以及為我們創建 this 對象的原因是,我們使用 new 關鍵字調用構造函數。如果在調用函數時不使用 new ,則該對象永遠不會創建,也不會隱式返回。我們可以在下面的例子中看到這個問題。
JavaScript 代碼:
function Animal (name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)
console.log(leo) // undefined
此模式的名稱是 Pseudoclassical Instantiation(偽類實例化) 。
如果 JavaScript 不是您的第一種程式語言,您可能會有點不安。
「WTF這個傢伙只是重新創造了一個更糟糕的版本」 – 你
對於那些不熟悉的人,Class(類) 允許您為對象創建模板。然後,無論何時創建該類的實例,都會獲得一個具有模板中定義的屬性和方法的對象。
聽起來有點熟悉?這基本上就是我們對上面的 Animal 構造函數所做的事情。但是對於 Animal 構造函數,我們只使用常規的舊 JavaScript 函數來重新創建相同的功能,而不是使用 class 關鍵字。當然,它需要一些額外的工作以及一些關於 JavaScript 「引擎」 所處理的事情的相關知識,但結果是一樣的。
這是個好消息。 JavaScript 不是一種 「死」 語言。它不斷得到改進,並由 TC-39委員會 不斷的制定標準。這意味著即使 JavaScript 的初始版本不支持類,也不影響後續將它們添加到官方規範中。事實上,這正是TC-39委員會所做的事情。 2015年,發布了 EcmaScript(官方JavaScript規範)6 ,支持 Classes(類) 和 class 關鍵字。讓我們看看上面的 Animal 構造函數如何使用新的 class(類) 語法。
JavaScript 代碼:
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 類的便捷語法,首先必須理解經典的偽類模式。
關於 ES6 Classes(類) 更多相關信息可以查看 面向對象的 JavaScript – 深入了解 ES6 類。
我們已經介紹了 JavaScript 原型的基礎知識。 本文的其餘部分將致力於加深理解相關知識的主題。 在另一篇文章中,我們將看看如何利用這些基礎知識並使用它們來理解 JavaScript 中繼承的工作原理。
數組方法我們在上面深入討論了如何在類的實例之間共享方法,您應該將這些方法放在類(或函數)原型上。 如果我們查看 Array 類,我們可以看到相同的模式。 從歷史上看,您可能已經創建了這樣的數組:
JavaScript 代碼:
const friends = []
事實證明,這只是創建一個新的 Array 類實例的語法糖。
JavaScript 代碼:
const friendsWithSugar = []
const friendsWithoutSugar = new Array()
您可能從未想過:數組的每個實例是如何具有所有內置方法的(splice , slice, pop 等)?
正如您現在所知,這是因為這些方法存在於 Array.prototype 上,當您創建一個新的 Array 實例時,您使用 new 關鍵字在查找失敗時將該委託設置為 Array.prototype 。
我們可以通過簡單地 console.log(Array.prototype) 來查看所有數組的方法。
JavaScript 代碼:
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()
*/
Objects(對象) 也是完全相同的邏輯。 所有對象將在查找失敗時委託給 Object.prototype ,這就是所有對象都有 toString 和hasOwnProperty 等方法的原因。
靜態方法到目前為止,我們已經介紹了為什麼,以及如何在類的實例之間共享方法。 但是,如果我們有一個對 Class 很重要但不需要又跨實例共享的方法,該怎麼辦呢? 例如,如果我們有一個函數,它接收一系列 Animal 實例並決定下一個需要餵食的對象,會怎樣? 我們將其稱為 nextToEat。
JavaScript 代碼:
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 類相同的作用域中,然後像我們平常那樣,在需要時引用它。
JavaScript 代碼:
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
現在這可行,但有更好的方法。
只要有一個特定於類本身的方法,但不需要在該類的實例之間共享,就可以將其添加為類的 static(靜態) 屬性。
JavaScript 代碼:
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 進行訪問。
JavaScript 代碼:
const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
console.log(Animal.nextToEat([leo, snoop])) // Leo
因為我們在這篇文章中都遵循了類似的模式,讓我們來看看如何使用 ES5 完成同樣的事情。 在上面的例子中,我們看到了如何使用 static 關鍵字將方法直接放在類本身上。 使用ES5,同樣的模式就像手動將方法添加到函數對象一樣簡單。
JavaScript 代碼:
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 方法完成獲取該對象的原型。
JavaScript 代碼:
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 prototype = Object.getPrototypeOf(leo)
console.log(prototype)
// {constructor: ?, eat: ?, sleep: ?, play: ?}
prototype === Animal.prototype // true
上面的代碼有兩個要點。
首先,你會注意到 proto 是一個對象,有4種方法,constructor,eat,sleep,和play。那講得通。我們將實例傳遞給getPrototypeOf,leo 獲取了實例的原型,這裡是所有的方法。這提示我們,關於原型的另外一件事我們還沒有討論過。默認情況下,原型對象將具有 constructor 屬性,該屬性指向原始函數或創建實例的類。這也意味著 JavaScript 默認在原型上放置 constructor 屬性,所以任何實例都可以通過 instance.constructor 訪問它們的構造函數。
上面的第二個要點是 Object.getPrototypeOf(leo) === Animal.prototype 。這也是有道理的。 Animal 構造函數有一個 prototype(原型) 屬性,我們可以在所有實例之間共享方法,getPrototypeOf 允許我們查看實例本身的原型。
JavaScript 代碼:
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.prototype 時, leo 沒有 prototype 屬性,因此它會將該查找委託給Animal.prototype,它確實具有 constructor 屬性。 如果這段沒有看懂,請回過頭來閱讀上面的 Object.create 。
您可能以前看到過使用 __proto__ 獲取實例的原型。 這是過去的遺物。 現在,如上所述使用 Object.getPrototypeOf(instance) 獲取實例的原型。
確定屬性是否存在於原型上在某些情況下,您需要知道屬性是否存在於實例本身上,還是存在於對象委託的原型上。 我們可以通過循環我們創建的 leo 對象來知道這一點。假設目標是循環 leo 並記錄其所有鍵和值。使用 for in 循環,可能看起來像這樣。
JavaScript 代碼:
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]}`)
}
你期望看到什麼?最有可能的是,它是這樣的 –
JavaScript 代碼:
Key: name. Value: Leo
Key: energy. Value: 7
但是,如果你運行代碼,你看到的是這樣的 –
JavaScript 代碼:
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 循環將循環遍歷對象本身以及它所委託的原型的所有 可枚舉屬性 。 因為默認情況下,您添加到函數原型的任何屬性都是可枚舉的,我們不僅會看到name 和 energy ,還會看到原型上的所有方法 – eat,sleep 和 play 。 要解決這個問題,我們需要指定所有原型方法都是不可枚舉的,或者如果屬性在 leo 對象本身上而不是 leo 查找失敗時委託給的原型上。 hasOwnProperty 可以幫助我們實現這個需求。
hasOwnProperty 是每個對象上的一個屬性,它返回一個布爾值,指示對象是否具有指定的屬性作為其自身的屬性,而不是對象委託給的原型。 這正是我們所需要的。 現在有了這些新知識,我們可以修改我們的代碼,以便利用 for in 循環中的 hasOwnProperty 。
JavaScript 代碼:
...
const leo = new Animal('Leo', 7)
for(let key in leo) {
if (leo.hasOwnProperty(key)) {
console.log(`Key: ${key}. Value: ${leo[key]}`)
}
}
而現在我們看到的只是 leo 對象本身的屬性,而不是 leo 原型中的方法。
JavaScript 代碼:
Key: name. Value: Leo
Key: energy. Value: 7
如果你仍然對 hasOwnProperty 感到困惑,這裡有一些代碼可以幫你消除困惑。
JavaScript 代碼:
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 運算符。 用例非常簡單,但如果您以前從未見過它,實際的語法有點奇怪。 它的工作原理如下
JavaScript 代碼:
object instanceof Class
如果 object 是 Class 的實例,則上面的語句將返回 true ,否則返回 false 。回到我們的 Animal 示例,我們會有類似的東西。
JavaScript 代碼:
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。
創建新的不可知構造函數你能發現下面代碼中的錯誤嗎?
JavaScript 代碼:
function Animal (name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)
即使是經驗豐富的 JavaScript 開發人員有時也會因為上面的例子而被絆倒。 因為我們正在使用之前學過的 pseudoclassical pattern(經典偽類模式),所以當調用 Animal 構造函數時,我們需要確保使用 new 關鍵字調用它。 如果我們不這樣做,則不會創建 this 關鍵字,也不會隱式返回。
作為複習,注釋掉的行是在函數上使用 new 關鍵字時幕後所做的事情。
JavaScript 代碼:
function Animal (name, energy) {
// const this = Object.create(Animal.prototype)
this.name = name
this.energy = energy
// return this
}
這似乎是一個非常重要的細節,讓其他開發人員記住。 假設我們正在與其他開發人員合作,有沒有辦法確保我們的 Animal 構造函數始終使用 new 關鍵字調用呢? 事實證明,它是通過使用我們之前學到的 instanceof 運算符來實現的。
如果使用 new 關鍵字調用構造函數,那麼構造函數體的內部將是構造函數本身的實例。 那是很多文字才能說清楚的。 這是一些代碼。
JavaScript 代碼:
function Animal (name, energy) {
if (this instanceof Animal === false) {
console.warn('Forgot to call Animal with the new keyword')
}
this.name = name
this.energy = energy
}
現在,如果我們使用 new 關鍵字重新調用函數,而不是只向函數的使用者列印警告,會發生什麼呢?
JavaScript 代碼:
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點開始吧。
JavaScript 代碼:
Object.create = function (objToDelegateTo) {
}
很簡單。
現在第2點 – 我們需要創建一個對象,該對象將在查找失敗時委託給參數對象。 這個有點棘手。 為此,我們將使用我們對 new 關鍵字和原型如何在 JavaScript 中工作的知識。首先,在 Object.create 實現的主體中,我們將創建一個空函數。 然後,我們將該空函數的原型設置為參數對象。然後,為了創建一個新對象,我們將使用 new 關鍵字調用空函數。如果我們返回新創建的對象,也會完成第3點。
JavaScript 代碼:
Object.create = function (objToDelegateTo) {
function Fn(){}
Fn.prototype = objToDelegateTo
return new Fn()
}
有點野蠻是吧?讓我們來看看吧。
當我們在上面的代碼中創建一個新函數 Fn 時,它帶有一個 prototype 屬性。 當我們使用 new 關鍵字調用它時,我們知道我們將得到的是一個對象,該對象將在查找失敗時委託給函數的原型。 如果我們覆蓋函數的原型,那麼我們可以決定在查找失敗時委託給哪個對象。 所以在我們上面的例子中,我們用調用 Object.create 時傳入的對象覆蓋 Fn的原型,我們稱之為 objToDelegateTo。
請注意,我們只支持 Object.create 的單個參數。官方實現還支持第二個可選參數,該參數允許您向創建的對象添加更多屬性。
箭頭函數箭頭函數沒有自己的 this 關鍵字。因此,箭頭函數不能用於構造函數,如果您嘗試使用 new 關鍵字調用箭頭函數,它將拋出錯誤。
JavaScript 代碼:
const Animal = () => {}
const leo = new Animal() // Error: Animal is not a constructor
另外,因為我們在上面證明了 pseudoclassical pattern(經典偽類模式) 不能與箭頭函數一起使用,所以箭頭函數也沒有 prototype(原型) 屬性。
JavaScript 代碼:
const Animal = () => {}
console.log(Animal.prototype) // undefined