JavaScript 原型的深入指南

来源:https://www.cnblogs.com/fundebug/archive/2019/04/24/10760764.html
-Advertisement-
Play Games

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


摘要: 理解prototype。

Fundebug經授權轉載,版權歸原作者所有。

不學會怎麼處理對象,你在 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

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

更進一步

現在我們知道三個點:

  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類,我們可以看到相同的模式。

const 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

關於Fundebug

Fundebug專註於JavaScript、微信小程式、微信小游戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • span設置部分省略...: span{ overflow: hidden; text-overflow: ellipsis; -o-text-overflow: ellipsis; white-space:nowrap; width:240px; height:24px; display:bloc ...
  • 1,如何將JS中ajax的非同步請求改為同步? Ajax請求預設的都是非同步的 如果想同步 async設置為false就可以(預設是true) var html = $.ajax({ url: "some.PHP", async: false }).responseText; 或者在全局設置Ajax屬性 ...
  • 今天我們來摺紙飛機(可以飛出去的那種哦) 基本全用css來實現,只有一小部分的js 首先看一下飛機的構造 灰色區域為可摺疊區域 白色區域為機身 三角形由border畫出來的再經過各種平移翻轉變成上圖 寫之前再補充個知識點: 我們顏色的設置不用rgba, 用hsl() h: 色調 0- 360 0(或 ...
  • 瀏覽器支持兩種類型的漸變:線性漸變 (linear-gradient),徑向漸變 (radial-gradient) 漸變在 CSS 中屬於一種 Image 類型,可以結合 background-image 屬性使用 和圖片不同的是,漸變不需要引入,而是由代碼直接生成的,所以比圖片更高效易用 一、簡 ...
  • 個人學習筆記 1.JQuery事件綁定 2.JQuery事件解綁 3.JQuery事件冒泡和預設行為和事件的自動觸發 4.JQuery自定義事件 5.JQuery事件的命名空間 6.JQuery事件委托 7.JQuery事件委托練習 8.JQuery的事件移入和移出 9.移入移出練習一 10.移入移 ...
  • CSSREM CSSREM 是一個CSS的 px 值轉 rem 值的Sublime Text3自動完成插件。先來看看插件的效果: 一個CSS的px值轉rem值的Sublime Text 3自動完成插件。 下載本項目,比如:git clone https://github.com/flashlizi/ ...
  • 今天在調試一個後臺的介面的時候,突然發現在network中找不到我發送的請求,也就是說,ajax沒有在瀏覽器的network中沒有顯示。。。。。第一次遇見這樣的情況,很苦惱,然後使用ajax中的error函數,看到了下麵的列印數據。 然後繼續網上百度“No Transport”的問題,發現網上大部分 ...
  • 在做單頁面應用程式時,一般頁面佈局頭尾兩塊都是固定在佈局頁面,中間為是路由入口。這時訪問頁面時頭部標題不會變,該問題的解決方案如下: 通過採用組件內路由衛士(beforeRouterEnter、beforeRouterUpdate)與路由元信息(meta) 來實現更新頭部標題信息。點擊查看文檔 be ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...