对象原型

原型是 JavaScript 对象相互继承特性的机制。在本文中,我们将解释什么是原型、原型链如何工作以及如何为一个对象设置原型。

预备知识 熟悉 JavaScript 基础知识(特别是对象基础)。
学习成果
  • JavaScript 原型链。
  • 属性遮蔽的概念。
  • 设置原型。
  • 原型和继承的概念。

原型链

在浏览器的控制台中,尝试创建一个对象字面量

js
const myObject = {
  city: "Madrid",
  greet() {
    console.log(`Greetings from ${this.city}`);
  },
};

myObject.greet(); // Greetings from Madrid

这是一个包含一个数据属性 city 和一个方法 greet() 的对象。如果你在控制台中输入对象的名称后跟一个句点,例如 myObject.,控制台将弹出一个该对象所有可用属性的列表。你会发现除了 citygreet 之外,还有许多其他属性!

__defineGetter__
__defineSetter__
__lookupGetter__
__lookupSetter__
__proto__
city
constructor
greet
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf

尝试访问其中一个

js
myObject.toString(); // "[object Object]"

它有效(即使 toString() 的作用不明显)。

这些额外的属性是什么,它们从何而来?

JavaScript 中的每个对象都有一个内置属性,称为其原型。原型本身也是一个对象,因此该原型也会有它自己的原型,从而形成所谓的原型链。当达到一个自身原型为 null 的原型时,链就结束了。

注意:指向其原型的对象属性称为 prototype。它的名称不规范,但在实践中所有浏览器都使用 __proto__。访问对象原型的标准方法是 Object.getPrototypeOf() 方法。

当你尝试访问对象的属性时:如果在对象本身中找不到该属性,则会在原型中搜索该属性。如果仍然找不到该属性,则会在原型的原型中搜索,依此类推,直到找到该属性,或者到达链的末尾,在这种情况下返回 undefined

所以当我们调用 myObject.toString() 时,浏览器会

  • myObject 中查找 toString
  • 在那里找不到,因此在 myObject 的原型对象中查找 toString
  • 在那里找到它,并调用它。

myObject 的原型是什么?要找出答案,我们可以使用函数 Object.getPrototypeOf()

js
Object.getPrototypeOf(myObject); // Object { }

这是一个名为 Object.prototype 的对象,它是最基本的原型,所有对象默认都拥有它。Object.prototype 的原型是 null,因此它位于原型链的末尾。

Prototype chain for myObject

对象的原型不总是 Object.prototype。试试这个

js
const myDate = new Date();
let object = myDate;

do {
  object = Object.getPrototypeOf(object);
  console.log(object);
} while (object);

// Date.prototype
// Object { }
// null

这段代码创建了一个 Date 对象,然后遍历原型链,记录原型。它向我们展示了 myDate 的原型是一个 Date.prototype 对象,而那个的原型是 Object.prototype

Prototype chain for myDate

事实上,当你调用熟悉的方法,如 myDate2.getTime() 时,你正在调用一个在 Date.prototype 上定义的方法。

属性遮蔽

如果在一个对象中定义一个属性,而该属性的名称与该对象的原型中定义的属性名称相同,会发生什么?让我们看看

js
const myDate = new Date(1995, 11, 17);

console.log(myDate.getTime()); // 819129600000

myDate.getTime = function () {
  console.log("something else!");
};

myDate.getTime(); // 'something else!'

考虑到原型链的描述,这应该是可以预测的。当我们调用 getTime() 时,浏览器首先在 myDate 中查找具有该名称的属性,并且只有在 myDate 未定义它时才检查原型。所以当我们向 myDate 添加 getTime() 时,就会调用 myDate 中的版本。

这被称为“遮蔽”属性。

设置原型

在 JavaScript 中有多种设置对象原型的方法,这里我们将介绍两种:Object.create() 和构造函数。

使用 Object.create

Object.create() 方法创建一个新对象,并允许你指定一个对象作为新对象的原型。

这是一个例子

js
const personPrototype = {
  greet() {
    console.log("hello!");
  },
};

const carl = Object.create(personPrototype);
carl.greet(); // hello!

这里我们创建了一个对象 personPrototype,它有一个 greet() 方法。然后我们使用 Object.create() 创建一个新对象,并将 personPrototype 作为其原型。现在我们可以在新对象上调用 greet(),并且原型提供了其实现。

使用构造函数

在 JavaScript 中,所有函数都有一个名为 prototype 的属性。当你将函数作为构造函数调用时,此属性被设置为新构造对象的原型(按照惯例,在名为 __proto__ 的属性中)。

所以如果我们设置构造函数的 prototype,我们可以确保所有用该构造函数创建的对象都拥有该原型。

js
const personPrototype = {
  greet() {
    console.log(`hello, my name is ${this.name}!`);
  },
};

function Person(name) {
  this.name = name;
}

Object.assign(Person.prototype, personPrototype);
// or
// Person.prototype.greet = personPrototype.greet;

这里我们创建

  • 一个对象 personPrototype,它有一个 greet() 方法
  • 一个 Person() 构造函数,它初始化要创建的人的姓名。

然后我们使用 Object.assignpersonPrototype 中定义的方法放置到 Person 函数的 prototype 属性上。

这段代码之后,使用 Person() 创建的对象将把 Person.prototype 作为它们的原型,该原型自动包含 greet 方法。

js
const reuben = new Person("Reuben");
reuben.greet(); // hello, my name is Reuben!

这也解释了我们之前为什么说 myDate 的原型被称为 Date.prototype:它是 Date 构造函数的 prototype 属性。

自有属性

我们使用上面 Person 构造函数创建的对象有两个属性

  • 一个 name 属性,它在构造函数中设置,因此直接出现在 Person 对象上
  • 一个 greet() 方法,它在原型中设置。

这种模式很常见,即方法在原型上定义,而数据属性在构造函数中定义。这是因为方法对于我们创建的每个对象通常都是相同的,而我们通常希望每个对象都有其数据属性的自己的值(就像这里每个人都有不同的姓名一样)。

直接在对象中定义的属性,例如这里的 name,被称为自有属性,你可以使用静态方法 Object.hasOwn() 检查一个属性是否是自有属性。

js
const irma = new Person("Irma");

console.log(Object.hasOwn(irma, "name")); // true
console.log(Object.hasOwn(irma, "greet")); // false

注意:你也可以在这里使用非静态方法 Object.hasOwnProperty(),但如果可以的话,我们建议你使用 Object.hasOwn()

原型和继承

原型是 JavaScript 中一个强大且非常灵活的特性,使得代码复用和对象组合成为可能。

特别是它们支持一种继承版本。继承是面向对象编程语言的一个特性,它允许程序员表达系统中某些对象是其他对象的更专业化版本的思想。

例如,如果我们正在模拟一所学校,我们可能有教授学生:他们都是,因此有一些共同的特性(例如,他们都有姓名),但每个人可能添加额外的特性(例如,教授有一个他们教授的科目),或者可能以不同的方式实现相同的特性。在 OOP 系统中,我们可以说教授和学生都继承自人。

你可以看到在 JavaScript 中,如果 ProfessorStudent 对象可以拥有 Person 原型,那么它们就可以继承共同的属性,同时添加和重新定义需要不同的属性。

在下一篇文章中,我们将讨论继承以及面向对象编程语言的其他主要特性,并了解 JavaScript 如何支持它们。

总结

本文涵盖了 JavaScript 对象原型,包括原型对象链如何允许对象相互继承特性、原型属性及其如何用于向构造函数添加方法以及其他相关主题。

在下一篇文章中,我们将探讨面向对象编程的基本概念。