ES 基础 —— 原型与原型链
原型
ES 中的每个对象都拥有一个内部属性 [[Prototype]]
,该内部属性就是原型。
理解原型
下列代码用来配合理解原型:
function Person() {
}
Person.prototype.name = "bill"
Person.prototype.age = 25
Person.prototype.job = "Nerd"
Person.prototype.sayName = function () {
console.log(this.name);
}
const person1 = new Person()
person1.sayName()
const person2 = new Person()
person2.sayName()
创建函数时,会:
-
根据特定的规则为该函数创建一个
prototype
属性,其中包含一个指针,指向函数的原型。 -
在默认情况下,所有的原型都会自动创建一个
constructor
属性,其中包含一个指针,指向prototype
属性所在的函数。
以上面的代码为例:
Person.prototype.constructor === Person // true
调用构造函数创建实例时,会为该实例创建内部属性 [[Prototype]]
,其中包含一个指针,指向构造函数的原型。
ES2015 中提供了
Object.setPrototypeOf()
和Object.getPrototypeOf()
来访问这个内部属性。但在以前,这个属性没有标准的访问方式:在 Firefox、 Chrome、 Safari 中每个对象都支持一个__proto__
;但在其他实现中,这个属性是完全不可见的)
为上述代码画一张图,来帮助理解:
+----------------------------------------------+
| |
| +-------------------------+-------+
| +----->| Person Prototype | |<----+------------------------------+
| | +----------------+--------+-------+ | |
v | | constructor | ## | | +-------------------------+-------+
+------------------------------+--+ +----------------+----------------+ | | person1 | |
| Person | | | name | "bill" | | +----------------+--------+-------+
+----------------+-------------+--+ +----------------+----------------+ | | [[Prototype]] | ## |
| prototype | #----+ | | age | 25 | | +----------------+----------------+
+----------------+----------------+ +----------------+----------------+ | +---------------------------------+
| job | "Nerd" | | | person2 |
+----------------+----------------+ | +----------------+----------------+
| sayName | [Function] | | | [[Prototype]] | ## |
+----------------+----------------+ | +----------------+--------+-------+
+------------------------------+
上图展示了 Person 构造函数、Person 的原型和 Person 现有的两个实例之间的关系。 当为实例添加一个属性时,这个属性就会屏蔽原型中保存的同名属性。使用 delete
运算符可以完全删除实例属性,从而可以重新访问原型中的属性。
原型与 in
运算符
有两种方式使用 in
运算符:
-
单独使用 —— 检测属性:
in
运算符会在通过对象能够访问给定属性时返回true
,无论该属性存在于实例中还是原型中。 -
在
for-in
循环中使用 —— 遍历属性:返回所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在与实例中的属性,也包括存在于原型中的属性。
原型的动态性
在原型中查找值的过程是一次搜索,因此对原型所做的任何修改都能立即从实例上反映出来,即使是先创建了实例,再修改原型也照样如此。看个例子:
function Person() {
}
const friend = new Person()
Person.prototype.sayHi = function () {
console.log("Hi")
}
friend.sayHi() // "Hi"
但,如果是重写整个原型,情况就不一样了。重写整个原型就等于:
- 切断了构造函数与最初原型之间的联系。
- 新原型与已经存在的实例间的不会有任何联系(已经存在的实例会一直引用最初的原型)。
function Person() {}
const friend = new Person()
Person.prototype = {
constructor: Person,
name: 'bill',
age: 29,
job: 'Nerd',
sayName: function () {
console.log(this.name)
},
}
friend.sayName() // TypeError: friend.sayName is not a function
在上面这个例子中,先创建了 Person
的一个实例,然后又重写了其原型。之后,在调用 friend.sayName()
时发生了错误,这是因为 friend
指向的原型中不包含 sayName
属性。
原型与原生对象
所有原生引用类型(Object、Array、String 等)都在其构造函数的原型上定义了方法。比如:
-
Array.prototype
中可以找到sort()
-
String.prototype
中可以找到substring()
- ……
通过原生对象的原型,不仅可以取得所有默认方法,而且还可以添加新方法或是修改已有的方法,比如:
String.prototype.newStartsWith = function (text) {
return this.indexOf(text) == 0
}
const msg = 'Hello world!'
console.log(msg.newStartsWith('Hello')) //true
尽管可以这样做,但并不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。
原型链
每个对象都包含一个指向它的原型的内部指针,原型也同样包含一个指向它自身的原型的指针,直到某个原型不再有原型为止。这种一级一级的链式结构就称为原型链(Prototype Chain)。
原型链的构建方式:将一个构造函数的实例赋值给另一个构造函数的原型。
实现原型链有一种基本模式,代码大致如下:
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承 SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
const instance = new SubType()
console.log(instance.getSuperValue()) // true
上述代码中,并没有使用 SubType
默认的原型,而是给它换了一个新原型 —— SuperType
的实例。这样,新原型不仅具有作为 SuperType
的实例所拥有的全部属性和方法,而且其内部还有一个指向 SuperType
原型的指针。最终结果就是,instance
有一个指向 SubType
的原型的指针,SubType
的原型又有一个指向 SuperType
原型的指针。
所有引用类型默认都继承了 Object,函数也不例外。所有函数的默认原型都是 Object 的实例,因此,默认原型都会包含一个内部指针,指向
Object.prototype
。这也正是所有自定义类型都会继承hasOwnProperty()
、isPrototypeOf()
等方法的根本原因。
上述代码中,实例、构造函数和原型之间的关系如下:
+----------------------------------------------------------------------+
v |
+-----------------------------------+ +-----------------------------------+ |
| Object | +----------->| Object Prototype | |
+-----------------+-----------------+ | +-----------------+-----------------+ |
| prototype | #--------+-+ | constructor | #--------+-+
+-----------------+-----------------+ | +-----------------+-----------------+
| | hasOwnProperty | [Function] |
| +-----------------+-----------------+
| | isPrototypeOf | [Function] |
| +-----------------+-----------------+
|
|
+----------------------------------------------------+
|
|
+----------------------------------------------------------------------+ |
v | |
+-----------------------------------+ +-----------------------------------+ | |
| SuperType | +----------->| SuperType Prototype | | |
+-----------------+-----------------+ | +-----------------+-----------------+ | |
| prototype | #--------+-+ | [[Prototype]] | #--------+-+-+
+-----------------+-----------------+ | +-----------------+-----------------+ |
| | constructor | #--------+-+
| +-----------------+-----------------+
| | getSuperValue | [Function] |
| +-----------------+-----------------+
|
|
|
+--------------------------------------------------+
+-----------------------------------+ +-----------------------------------+ |
| SubType | +----------->| SubType Prototype | |
+-----------------+-----------------+ | +-----------------+-----------------+ |
| prototype | #--------+-+ | [[Prototype]] | #--------+-+
+-----------------+-----------------+ | +-----------------+-----------------+
| | property | true |
| +-----------------+-----------------+
| | getSubValue | [Function] |
| +-----------------+-----------------+
|
+-----------------------------------+ |
| instance | |
+-----------------+-----------------+ |
| [[Prototype]] | #--------+-+
+-----------------+-----------------+
| subproperty | false |
+-----------------+-----------------+
原型链本质上是拓展了原型搜索机制。为什么这么讲?当读取一个实例的属性时,首先会在实例中搜索该属性;如果没有找到该该属性,则会继续搜索实例的原型;如果仍然没有找到,会沿着原型链继续向上搜索。直到原型链末端才会结束。
谨慎地定义方法
子类有时候要添加超类中不存在的某个方法,或者重写超类中的某个方法。但无论如何,操作原型中的方法时一定要放在替换原型的语句之后,否则,定义的方法是存在于将要被替换掉的原型对象中的,而这个原型对象之后是会被父类的实例替换掉的,这也就导致定义的方法就直接消失了。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype.getSubValue = function () {
return this.subproperty
}
// 继承 SuperType
SubType.prototype = new SuperType()
var instance = new SubType();
console.log(instance.getSubValue()); // Reference Error
还有一点,通过原型链实现继承时,不要使用对象字面量创建原型方法,因为这也会重写原型。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承 SuperType
SubType.prototype = new SuperType()
// 使用对象字面量添加新方法,切断了 SubType 的原型和 SuperType 的原型之间的联系
SubType.prototype = {
getSubValue: function () {
return this.subproperty
}
}
const instance = new SubType()
console.log(instance.getSuperValue()) // Reference Error
确定原型和实例的关系
可以通过几种方式来确定原型和实例之间的关系。
instanceof
运算符
只要测试实例与原型链中出现过的原型所对应的构造函数,就会返回 true
。
console.log(instance instanceof Object) // true
console.log(instance instanceof SuperType) // true
console.log(instance instanceof SubType) // true
isPrototypeOf()
方法
只要测试实例与原型链中出现过的原型,就会返回 true
。
console.log(Object.prototype.isPrototypeOf(instance)) // true
console.log(SuperType.prototype.isPrototypeOf(instance)) // true
console.log(SubType.prototype.isPrototypeOf(instance)) // true
其他方法
-
Object.getPrototypeOf()
-
instance.constructor.name