发布于 2026-01-06 1 阅读
0

JavaScript原型的神奇世界

JavaScript原型的神奇世界

我们听过多少次“JavaScript 不是面向对象的语言,而是面向原型的语言”这种说法?事实证明,这种说法并不准确。

这里有一些 JavaScript 对象,每个对象都是以不同的方式创建的:

({} instanceof Object)
// => true

([] instanceof Object)
// => true

function Foo() {}
((new Foo) instanceof Object)
// => true

所以,JavaScript 中确实有对象。那么,原型呢?原型是 JavaScript 实现面向对象特性的机制。因此,JavaScript 是一种基于原型的面向对象语言

随着 ES6 类的出现,有些人可能会认为学习如何处理原型已经没有意义了。这种想法是错误的,原因有以下几点:

  1. ES6 类本质上是原型的一种语法糖。ES6“类”的实例仍然是基于原型的。

  2. 世界上存在着庞大的 ES5(即没有类)代码库,你迟早都会遇到它。

接下来,让我们来学习一下 JavaScript 原型吧?


原型只是一个嵌入在对象中的“特殊对象”。在 JavaScript 中,我们可以通过属性访问它__proto__

const witch = { name: "Hermione" }
witch.__proto__
// => {} (empty prototype)

它的特别之处在于,原型充当了一种“代理”或“备份”的角色,而且是透明的。如果我们尝试访问对象中不存在的属性,但原型中存在该属性,JavaScript 将返回原型的值。继续之前的例子:

// add a property to the prototype
witch.__proto__.spells = { leviosa: "Wingardium leviosa" }

// the property is not defined by the object…
witch
// => { name: "Hermione" }

// …but we can access it thanks to the prototype!
witch.spells
// => { leviosa: "Wingardium leviosa" }

这有什么实际应用呢?就是在对象之间共享代码。在面向对象的语言中,类就像一个“模板”,所有该类的实例都可以共享这个模板。而在 JavaScript 中,没有“模板”:我们拥有的是一个共享的公共对象,即原型。

当我们使用构造函数实例化对象时,这一点很容易理解。如果我们有一个Wizard构造函数,每次使用它创建一个新对象时new Wizard(),属性中定义的值Wizard.prototype都会被确立为新创建实例的原型。

function Wizard(name) {
  this.name = name || "Anonymous"
}

Wizard.prototype.spells = {
  leviosa: "Wingardium leviosa",
  expelliarmus: "Expelliarmus",
  patronus: "Expecto patronum" 
}

const draco = new Wizard("Draco")
// => Wizard { name: "Draco" }
const hermione = new Wizard("Hermione")
// => Wizard { name: "Hermione" }

draco.spells === hermione.spells
// => true (both wizards share spells)
draco.__proto__ === hermione.__proto__
// => true (that's why they share prototypes)
hermione.__proto__ === Wizard.prototype
// => true (their prototype is defined in Wizard.prototype)

共享原型这一公共对象的好处有:

  • 为了避免内存重复,因为原型由所有需要它的对象共享,而不是每个对象都拥有一个原型的副本。
  • 通过修改原型,可以一次性动态修改多个对象。

借助此系统,我们还可以仅修改特定对象,为其添加专属属性。如果该属性与原型中的某个属性同名,则对象中直接包含的属性优先。例如,我们可以创建一个霍格沃茨一年级新生,他的魔法书是空白的:

const newbie = new Wizard("Lorem")
newbie.spells = {} // bypass what's in the prototype

newbie.spells === hermione.spells
// => false

现在让我们想象一下,在魔法世界里,一项重大发现诞生了:他们学会了随时变出正宗的西班牙米浆饮料(horchata)。只要魔法书的版本没有被覆盖,我们就可以轻松地更新每个人的魔法书,只需修改原型本身即可。

// add a new spell
Wizard.prototype.spells.horchata = "Send horchata"

// check Hermione's spellbook
hermione.spells
// => { leviosa: "Windgardium leviosa",
//   expelliarmus: "Expelliarmus",
//   patronus: "Expecto patronum",
//   horchata: "Send horchata" }

这是一个非常强大的功能,但多亏了漫威,我们都知道能力越大,责任越大。在 JavaScript 中更是如此,因为修改原型太容易了。我们能做到什么程度呢?我们甚至可以修改标准库中对象的原型,例如 `Object` ObjectDate`Object`、Array`Object`……下面是一个我称之为“弗拉门戈技巧”的简单示例:

Date.prototype.toString = () => "💃"
`${new Date()}`
// => 💃

希望您喜欢这篇关于 JavaScript 原型的简短介绍。祝您编程愉快!

文章来源:https://dev.to/ladybenko/the-magical-world-of-javascript-prototypes-1mhg