对象原型

原型是 JavaScript 对象彼此继承特性的机制。本文将解释什么是原型、原型链是如何工作的以及如何设置对象的原型。

先决条件 了解 JavaScript 函数,熟悉 JavaScript 基础知识(参见 第一步构建模块),以及 OOJS 基础知识(参见 对象简介)。
目标 了解 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 对象原型,包括原型对象链如何允许对象彼此继承特性、原型属性以及如何使用它向构造函数添加方法,以及其他相关主题。

在下一篇文章中,我们将介绍面向对象编程背后的概念。