对象原型

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

¥Prototypes are the mechanism by which JavaScript objects inherit features from one another. In this article, we explain what a prototype is, how prototype chains work, and how a prototype for an object can be set.

先决条件: 了解 JavaScript 函数,熟悉 JavaScript 基础知识(请参阅 第一步架构模块)和 OOJS 基础知识(请参阅 对象介绍)。
目标: 了解 JavaScript 对象原型、原型链如何工作以及如何设置对象的原型。

原型链

¥The prototype chain

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

¥In the browser's console, try creating an object literal:

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

myObject.greet(); // Greetings from Madrid

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

¥This is an object with one data property, city, and one method, greet(). If you type the object's name followed by a period into the console, like myObject., then the console will pop up a list of all the properties available to this object. You'll see that as well as city and greet, there are lots of other properties!

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

尝试访问其中之一:

¥Try accessing one of them:

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

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

¥It works (even if it's not obvious what toString() does).

这些额外的属性是什么?它们从哪里来?

¥What are these extra properties, and where do they come from?

JavaScript 中的每个对象都有一个内置属性,称为原型。原型本身就是一个对象,因此原型将有自己的原型,形成所谓的原型链。当我们到达一个具有 null 原型的原型时,链就结束了。

¥Every object in JavaScript has a built-in property, which is called its prototype. The prototype is itself an object, so the prototype will have its own prototype, making what's called a prototype chain. The chain ends when we reach a prototype that has null for its own prototype.

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

¥Note: The property of an object that points to its prototype is not called prototype. Its name is not standard, but in practice all browsers use __proto__. The standard way to access an object's prototype is the Object.getPrototypeOf() method.

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

¥When you try to access a property of an object: if the property can't be found in the object itself, the prototype is searched for the property. If the property still can't be found, then the prototype's prototype is searched, and so on until either the property is found, or the end of the chain is reached, in which case undefined is returned.

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

¥So when we call myObject.toString(), the browser:

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

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

¥What is the prototype for myObject? To find out, we can use the function Object.getPrototypeOf():

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

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

¥This is an object called Object.prototype, and it is the most basic prototype, that all objects have by default. The prototype of Object.prototype is null, so it's at the end of the prototype chain:

Prototype chain for myObject

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

¥The prototype of an object is not always Object.prototype. Try this:

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

¥This code creates a Date object, then walks up the prototype chain, logging the prototypes. It shows us that the prototype of myDate is a Date.prototype object, and the prototype of that is Object.prototype.

Prototype chain for myDate

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

¥In fact, when you call familiar methods, like myDate2.getTime(), you are calling a method that's defined on Date.prototype.

阴影属性

¥Shadowing properties

如果在对象中定义属性,并且在对象的原型中定义同名属性,会发生什么情况?让我们来看看:

¥What happens if you define a property in an object, when a property with the same name is defined in the object's prototype? Let's see:

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 未定义原型时才检查原型。所以当我们将 getTime() 添加到 myDate 中时,那么就会调用 myDate 中的版本。

¥This should be predictable, given the description of the prototype chain. When we call getTime() the browser first looks in myDate for a property with that name, and only checks the prototype if myDate does not define it. So when we add getTime() to myDate, then the version in myDate is called.

这就是所谓的 "shadowing" 属性。

¥This is called "shadowing" the property.

设定原型

¥Setting a prototype

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

¥There are various ways of setting an object's prototype in JavaScript, and here we'll describe two: Object.create() and constructors.

使用对象.create

¥Using Object.create

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

¥The Object.create() method creates a new object and allows you to specify an object that will be used as the new object's prototype.

这是一个例子:

¥Here's an example:

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

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

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

¥Here we create an object personPrototype, which has a greet() method. We then use Object.create() to create a new object with personPrototype as its prototype. Now we can call greet() on the new object, and the prototype provides its implementation.

使用构造函数

¥Using a constructor

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

¥In JavaScript, all functions have a property named prototype. When you call a function as a constructor, this property is set as the prototype of the newly constructed object (by convention, in the property named __proto__).

因此,如果我们设置构造函数的 prototype,我们可以确保使用该构造函数创建的所有对象都被赋予该原型:

¥So if we set the prototype of a constructor, we can ensure that all objects created with that constructor are given that 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;

在这里我们创建:

¥Here we create:

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

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

¥We then put the methods defined in personPrototype onto the Person function's prototype property using Object.assign.

执行此代码后,使用 Person() 创建的对象将获得 Person.prototype 作为其原型,其中自动包含 greet 方法。

¥After this code, objects created using Person() will get Person.prototype as their prototype, which automatically contains the greet method.

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

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

¥This also explains why we said earlier that the prototype of myDate is called Date.prototype: it's the prototype property of the Date constructor.

自有属性

¥Own properties

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

¥The objects we create using the Person constructor above have two properties:

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

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

¥It's common to see this pattern, in which methods are defined on the prototype, but data properties are defined in the constructor. That's because methods are usually the same for every object we create, while we often want each object to have its own value for its data properties (just as here where every person has a different name).

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

¥Properties that are defined directly in the object, like name here, are called own properties, and you can check whether a property is an own property using the static Object.hasOwn() method:

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

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

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

¥Note: You can also use the non-static Object.hasOwnProperty() method here, but we recommend that you use Object.hasOwn() if you can.

原型和继承

¥Prototypes and inheritance

原型是 JavaScript 的一项强大且非常灵活的功能,使重用代码和组合对象成为可能。

¥Prototypes are a powerful and very flexible feature of JavaScript, making it possible to reuse code and combine objects.

特别是它们支持继承的版本。继承是面向对象编程语言的一项功能,它允许程序员表达这样的想法:系统中的某些对象是其他对象的更专业的版本。

¥In particular they support a version of inheritance. Inheritance is a feature of object-oriented programming languages that lets programmers express the idea that some objects in a system are more specialized versions of other objects.

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

¥For example, if we're modeling a school, we might have professors and students: they are both people, so have some features in common (for example, they both have names), but each might add extra features (for example, professors have a subject that they teach), or might implement the same feature in different ways. In an OOP system we might say that professors and students both inherit from people.

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

¥You can see how in JavaScript, if Professor and Student objects can have Person prototypes, then they can inherit the common properties, while adding and redefining those properties which need to differ.

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

¥In the next article we'll discuss inheritance along with the other main features of object-oriented programming languages, and see how JavaScript supports them.

概括

¥Summary

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

¥This article has covered JavaScript object prototypes, including how prototype object chains allow objects to inherit features from one another, the prototype property and how it can be used to add methods to constructors, and other related topics.

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

¥In the next article we'll look at the concepts underlying object-oriented programming.