对象原型
原型是 JavaScript 对象彼此继承特性的机制。本文将解释什么是原型、原型链是如何工作的以及如何设置对象的原型。
原型链
在浏览器的控制台中,尝试创建一个对象字面量
const myObject = {
city: "Madrid",
greet() {
console.log(`Greetings from ${this.city}`);
},
};
myObject.greet(); // Greetings from Madrid
这是一个包含一个数据属性 city
和一个方法 greet()
的对象。如果你在控制台中输入对象的名称,后面跟着一个点,比如 myObject.
,那么控制台将弹出一个包含此对象所有可用属性的列表。你会看到除了 city
和 greet
之外,还有很多其他属性!
__defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ __proto__ city constructor greet hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf
尝试访问其中一个
myObject.toString(); // "[object Object]"
它起作用了(即使不清楚 toString()
是做什么的)。
这些额外的属性是什么,它们来自哪里?
JavaScript 中的每个对象都具有一个内置属性,称为其 **原型**。原型本身是一个对象,因此原型将有自己的原型,从而形成所谓的 **原型链**。当我们到达原型链的末尾,即原型自身的原型为 null
时,链条就会结束。
**注意:** 指向对象原型的对象属性**不**叫 prototype
。它的名称没有标准,但实际上所有浏览器都使用 __proto__
。访问对象原型的标准方法是 Object.getPrototypeOf()
方法。
当你尝试访问对象的属性时:如果在对象本身中找不到属性,则会在原型中搜索该属性。如果仍然找不到属性,则会在原型的原型中搜索,依此类推,直到找到该属性,或者到达链条的末尾,在这种情况下将返回 undefined
。
所以当我们调用 myObject.toString()
时,浏览器
- 在
myObject
中查找toString
- 在那里找不到,因此在
myObject
的原型对象中查找toString
- 在那里找到它并调用它。
myObject
的原型是什么?为了找到答案,我们可以使用 Object.getPrototypeOf()
函数
Object.getPrototypeOf(myObject); // Object { }
这是一个名为 Object.prototype
的对象,它是所有对象默认具有的最基本原型。Object.prototype
的原型是 null
,因此它位于原型链的末尾
对象的原型并不总是 Object.prototype
。试试这个
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
。
事实上,当你调用熟悉的方法,比如 myDate2.getTime()
时,你是在调用 Date.prototype
上定义的方法。
遮蔽属性
如果你在对象中定义了一个属性,而该属性的名称在对象的原型中也定义了,会发生什么情况?让我们看看
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()
方法创建一个新对象,并允许你指定一个用作新对象原型的对象。
以下是一个示例
const personPrototype = {
greet() {
console.log("hello!");
},
};
const carl = Object.create(personPrototype);
carl.greet(); // hello!
这里我们创建了一个对象 personPrototype
,它有一个 greet()
方法。然后我们使用 Object.create()
创建一个新的对象,其原型为 personPrototype
。现在,我们可以对新对象调用 greet()
,原型将提供其实现。
使用构造函数
在 JavaScript 中,所有函数都具有一个名为 prototype
的属性。当你将函数作为构造函数调用时,此属性将设置为新构造对象的原型(按照惯例,在名为 __proto__
的属性中)。
因此,如果我们设置了构造函数的 prototype
,我们可以确保使用该构造函数创建的所有对象都将获得该原型
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.assign 将 personPrototype
中定义的方法放到 Person
函数的 prototype
属性上。
在这段代码之后,使用 Person()
创建的对象将获得 Person.prototype
作为其原型,该原型自动包含 greet
方法。
const reuben = new Person("Reuben");
reuben.greet(); // hello, my name is Reuben!
这也解释了为什么我们之前说 myDate
的原型叫做 Date.prototype
:它是 Date
构造函数的 prototype
属性。
自身属性
我们使用上面的 Person
构造函数创建的对象有两个属性
- 一个
name
属性,它在构造函数中设置,因此它直接出现在Person
对象上 - 一个
greet()
方法,它在原型中设置。
这种模式很常见,其中方法定义在原型上,但数据属性在构造函数中定义。这是因为方法通常对我们创建的每个对象都是相同的,而我们通常希望每个对象都具有其数据属性的自身值(就像这里每个人的名称都不同一样)。
直接在对象中定义的属性,例如这里的 name
,被称为 **自身属性**,你可以使用静态 Object.hasOwn()
方法检查属性是否为自身属性
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 中,如果 Professor
和 Student
对象可以具有 Person
原型,那么它们可以继承公共属性,同时添加和重新定义需要不同的那些属性。
在下一篇文章中,我们将讨论继承以及面向对象编程语言的其他主要特性,并了解 JavaScript 如何支持它们。