ES 基础 —— 原型与原型链

published

原型

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