# JS的原型链和继承
Table of Contents
JavaScript 的原型链和相关继承方式。
原型 prototype
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,该对象包含了所有实例共享的属性和方法。
我们需要明白对象原型(Object.getPrototypeOf(obj),或者通过已弃用的 __proto__ 属性)和构造函数的 prototype 属性之间有区别。
constructor
constructor 属性始终指向创建当前对象的构建函数。在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。
function Foo(name) { this.name = name}
const f1 = new Foo('bar')const f2 = Object.create(f1)
console.log(Object.getPrototypeOf(f1))// { constructor: function Foo(name) }
console.log(Object.getPrototypeOf(f2))// { name: 'bar' }
console.log(f1.constructor, f2.constructor)// function Foo(name), function Foo(name)可以看出 f2.prototype 原型指向 f1。f2 的 constructor 指向的是构造函数。将构建函数的 prototype 进行扩展
Foo.prototype.age = 11Foo.prototype.dosomething = function () { // ...}Foo: 构造函数Foo Prototype: 原型对象f1, f2: 实例对象 ┌───────────────────────────────────────────────┐ ▼ │ ┌──────────────────────┐ ┌─────────────────────────┐ │ │ Foo │ ┌──►│ Foo Prototype │ │ ├──────────┬───────────┤ │ ├────────────┬────────────┤ │ │prototype │ ├───┤ │constructor │ ├─┘ ├──────────┼───────────┤ │ ├────────────┼────────────┤ │name │(undefined)│ │ │age │11 │ └──────────┴───────────┘ │ ├────────────┼────────────┤ │ │dosomething │(function) │ ┌─────────────────────────┐ │ └────────────┴────────────┘┌►│ f1 │ ││ ├─────────────────────────┤ ││ │Object.getPrototypeOf(f1)├───┘│ ├─────────────┬───────────┤│ │name │'bar' ││ └─────────────┴───────────┘││ ┌─────────────────────────┐│ │ f2 ││ ├─────────────────────────┤└─┤Object.getPrototypeOf(f2)│ └─────────────────────────┘我们可以发现实例对象 f1, f2 和构造函数没有直接关系。f1,f2 的 constructor 指向构造函数而已。
继承方式
JavaScript 主要有如下几种继承方式:
- 原型链
- 借用构造函数
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链
实现原型链继承,就是让一个对象的原型对象指向另一个对象的实例,实现的本质就是重写原型对象。
function Person() { this.name = 'human'}
function Student() {}
// 重写 Student 的原型,继承 Person 实例Student.prototype = new Person()
var instance = new Student()console.log(instance.name)重写原型对象的时候,默认自带的 constructor 会被覆盖,这个实例的 constructor 会指向构造函数 Person。
注意要点:
- 重写原型链上的方法会屏蔽原来的方法
- 不能使用对象字面量创建原型方法
问题:
- 包含引用类型值的原型属性会被所有实例共享。修改了该属性,所有实例的属性也会改变
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数
借用构造函数
借用构造函数方法,也叫伪造对象或经典继承,基本思想是在子类型构造函数的内部调用超类型构造函数。
通过使用 apply 和 call 方法可以在新创建的对象上执行构造函数
function Person() { this.name = 'human'}
function Student() { Person.call(this)}
var instance = new Student()console.log(instance.name)传递参数
function Sup(name) { this.name = name}function Sub() { Sup.call(this, 'foo') this.age = 18}var instance = new Sub()console.log(instance.name) // foo方法在构造函数中定义,函数的复用无法实现。
组合继承(最常用)
也叫伪经典继承,就是将原型链和借用构造函数组合一起。
其实通过上面的两个继承方式,我们发现有意思的地方。原型链能够很好地复用方法,但是会无法传递参数,而借用构造函数可以传递参数,却无法复用方法。所以我们将他们组合到一起。
基本思想是,通过借用构造函数来实现对实例属性的继承,通过在原型链上定义方法实现了函数复用。
function Sup(name) { this.name = name; this.colors = ['red', 'blue'];}
Sup.prototype.sayName = funtion() { console.log(this.name);}
// 构造函数实现实例属性的继承function Sub(name, age) { Sup.call(this, name); this.age = age;}
// 原型链复用方法Sub.prototype = new Sup();Sub.prototype.constructor = Sub;
const instance1 = new Sub("foo", 18);instance1.colors.push('black'); // ['red', 'blue', 'black']instance1.sayName(); // 'foo'
const instance2 = new Sub('bar', 20);console.log(instance2.colors); // ['red', 'blue']instance2.sayName(); // 'bar'让两个不同的 Sub 实例既拥有自己的属性 — 包括 colors 属性,又可以使用相同的方法。
原型式继承
借助原型可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型。
function object(o) { function F() {} F.prototype = o return new F()}ES5 通过 Object.create() 方法规范化了原型式继承。
注:包含引用类型值的原型属性会被所有实例共享。就像使用原型模式一样。
寄生式继承
寄生式继承的思路与工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function createAnother(original) { var clone = object(original) // 创建一个新对象,其他方式返回新对象也可 clone.say = function () { // ... } return clone}寄生组合式继承
function inheritPrototype(sub, sup) { var prototype = Object(sup.prototype) // 创建对象 prototype.constructor = sub sub.prototype = prototype}参考资料
- 《JavaScript 高级程序设计》
- MDN: Object prototypes