JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null
作为其原型的对象上。
指向对象原型的属性并不是 prototype
。它的名字不是标准的,但实际上所有浏览器都使用 __proto__
。访问对象原型的标准方法是 Object.getPrototypeOf()
。
当你试图访问一个对象的属性时:如果在对象本身中找不到该属性,就会在原型中搜索该属性。如果仍然找不到该属性,那么就搜索原型的原型,以此类推,直到找到该属性,或者到达链的末端,在这种情况下,返回 undefined
。
如果你在一个对象中定义了一个属性,而在该对象的原型中定义了一个同名的属性,会发生什么?我们来看看:
鉴于对原型链的描述,这应该是可以预测的。当我们调用 getYear()
时,浏览器首先在 myDate
中寻找具有该名称的属性,如果 myDate
没有定义该属性,才检查原型。因此,当我们给 myDate
添加 getYear()
时,就会调用 myDate
中的版本。
这叫做属性的“遮蔽”。
在 JavaScript 中,有多种设置对象原型的方法,这里我们将介绍两种:Object.create()
和构造函数。
Object.create()
方法创建一个新的对象,并允许你指定一个将被用作新对象原型的对象。
这里我们创建了一个 personPrototype
对象,它有一个 greet()
方法。然后我们使用 Object.create()
来创建一个以 personPrototype
为原型的新对象。现在我们可以在新对象上调用 greet()
,而原型提供了它的实现。
在 JavaScript 中,所有的函数都有一个名为 prototype
的属性。当你调用一个函数作为构造函数时,这个属性被设置为新构造对象的原型(按照惯例,在名为 __proto__
的属性中)。
因此,如果我们设置一个构造函数的 prototype
,我们可以确保所有用该构造函数创建的对象都被赋予该原型:
这里我们:
personPrototype
对象,它具有 greet()
方法Person()
构造函数,它初始化了要创建人物对象的名字然后我们使用 Object.assign 将 personPrototype
中定义的方法绑定到 Person
函数的 prototype
属性上。
在这段代码之后,使用 Person()
创建的对象将获得 Person.prototype
作为其原型,其中自动包含 greet
方法。
这也解释了为什么我们之前说 myDate
的原型被称为 Date.prototype
:它是 Date
构造函数的 prototype
属性。
我们使用上面的 Person
构造函数创建的对象有两个属性:
name
属性,在构造函数中设置,在 Person
对象中可以直接看到greet()
方法,在原型中设置我们经常看到这种模式,即方法是在原型上定义的,但数据属性是在构造函数中定义的。这是因为方法通常对我们创建的每个对象都是一样的,而我们通常希望每个对象的数据属性都有自己的值(就像这里每个人都有不同的名字)。
直接在对象中定义的属性,如这里的name
,被称为自有属性,你可以使用静态方法 Object.hasOwn()
检查一个属性是否是自有属性:
备注: 你也可以在这里使用非静态方法 Object.hasOwnProperty()
,但我们推荐尽可能使用 Object.hasOwn()
方法。
原型是 JavaScript 的一个强大且非常灵活的功能,使得重用代码和组合对象成为可能。
特别是它们支持某种意义的继承。继承是面向对象的编程语言的一个特点,它让程序员表达这样的想法:系统中的一些对象是其他对象的更专门的版本。
例如,如果我们正在为一所学校建模,我们可能有教授和学生:他们都是人,所以有一些共同的特征(例如,他们都有名字),但每个人都可能增加额外的特征(例如,教授有一个他们所教的科目),或者可能以不同的方式实现同一个特征。在一个 OOP 系统中,我们可以说教授和学生都继承自人。
你可以看到在 JavaScript 中,如果 Professor
和 Student
对象具有原型 Person
,那么他们可以继承共同的属性,同时增加和重新定义那些需要不同的属性。