# 原型链
原型链是实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法,也就是原型对象等于另一个类型的实例。 实现原型链有一个基本模式,其代码大致如下:
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承了 SuperType
// 这里需要注意的是 new SuperType 进行了实例化,实例化有一个 __proto__ 属性,指向的是 Supertype 的原型对象
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function() {
return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true
console.log(instance.constructor) // ƒ SuperType() { this.property = true }
上面的代码最终实现了一个这样的效果:instance 的__proro__ 指向 SubType 的原型,SubType 的原型的原型对象的__proro__ 属性又指向 SuperType 的原型,getSuperValue 方法仍然还在 SuperType.prototype 中,但 property 则位于 SubType.prototype 中,这是因为 property 是一个实例属性,而 getSuperValue 则是一个原型方法;既然 SubType.prototype 现在是 SuperType 的实例,那么 property 当然就位于该实例中了。此外,要注意的是 instance.constructor 现在指向的是SuperType,这是因为原来 SubType.prototype 中的 constructor 被重写的缘故。
# 默认的原型
其实我们上面实现的继承是少了一环节,我们知道所有的引用类型默认都继承了 Object,而这个继承也是通过原型链实现的,要记住,所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString 方法以及 valueOf 方法等默认方法的根本原因;
# 确定原型和实例之间的关系
可以通过两种方式来确定原型和实例之间的关系:
- 使用 instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数
结果都会返回 true, 如下:
console.log(instance instanceof Object) // true
console.log(instance instanceof SuperType) // true
console.log(instance instanceof SubType) // true
// 由于原型链的关系,我们可以说 instance 是 Object、SuperType、SubType 中任何一个类型的实例,因此,测试这三个构造函数的结果都返回 true
- 使用 isPrototypeOf 方法,同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
因此 isPrototypeOf 方法都会返回 true:
console.log(Object.prototype.isPrototypeOf(instance)) // true--也就是说 instance 调用了 Object 的原型,也就是 Object.prototype
console.log(SuperType.prototype.isPrototypeOf(instance)) // true
console.log(SubType.prototype.isPrototypeOf(instance)) // true
# 谨慎地定义方法
有时候需要添加超类型中不存在的某个方法,或者是需要覆盖超类型中的某个方法,给原型添加方法的代码一定要放在替换原型的语句之后、比如下面的例子:
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承了 SuperType
// 这里需要注意的是 new SuperType 进行了实例化,实例化有一个 __proto__ 属性,指向的是 Supertype 的原型对象
SubType.prototype = new SuperType()
/**
这里需要注意的是必须在用 SuperType 的实例替换原型之后,再定义下面的这两个方法
*/
// 添加新方法
SubType.prototype.getSubValue = function() {
return this.subproperty
}
// 重写超类型中的方法
SubType.prototype.getSuperValue = function() {
return false
}
console.log(SubType.prototype) // SuperType {property: true, getSubValue: ƒ, getSuperValue: ƒ}
console.log(SuperType.prototype) // {getSuperValue: ƒ, constructor: ƒ}
const instance = new SubType()
console.log(instance.getSuperValue()) // false---返回的是重写后的方法
这里有一点需要注意的是,在通过原型链实现继承的时候,不能使用对象字面量创建原型方法,因为这样会重写原型链,如下面的例子:
function SuperType() {
this.property=true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承了 SuperType
// 这里需要注意的是 new SuperType 进行了实例化,实例化有一个 __proto__ 属性,指向的是 Supertype 的原型对象
SubType.prototype = new SuperType()
// 使用字面量添加新方法,会导致上一行代码无效,
// 现在的原型包含的是一个 Object 实例,而非 SuperType 的实例---切断了 SubType 与 SuperType 之前的联系
SubType.prototype = {
getSubValue() {
return this.subproperty
},
someOtherMethid() {
return false
}
}
const instance = new SubType()
console.log(instance.getSuperValue()) // instance.getSuperValue is not a function
# 原型链的问题
主要的问题来自包含引用类型值的原型,包含引用类型值的原型属性会被所有实例所共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因,在通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。比如下面的代码:
function SuperType() {
this.color = ['red','blue','green'] // 引用类型
}
function SubType() {}
// 继承了 SuperType
// 这里需要注意的是new SuperType进行了实例化,实例化有一个 __proto__ 属性,指向的是 Supertype 的原型对象
SubType.prototype = new SuperType()
const instance = new SubType()
// SubType 的所有实例都会共享 color 这个属性
instance.color.push('white')
console.log(instance.color) // ["red", "blue", "green", "white"]
const instance2 = new SubType()
console.log(instance2.color) // ["red", "blue", "green", "white"]
第二个存在的问题就是:在创建子类型的实例时,不能向超类型的构造函数中传递参数,实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数,
# 借用构造函数
该技术的基本思想就是在子类型构造函数的内部调用超类型构造函数。这里需要注意的是,函数只不过是在特定环境中执行代码的对象;因此通过 apply,和 call 方法也可以在新创建的对象上执行构造函数,如下面的代码:
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
// 继承了 SuperType
// 新创建的 SubType 实例的环境下调用了 SuperType 构造函数
SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "blue", "green", "black"]
var instance2 = new SubType()
console.log(instance2.colors) // ["red", "blue", "green"]
# 传递参数
当然 call 方法也可以进程传递参数:
function SuperType(name) {
this.name = name
}
function SubType() {
// 继承了 SuperType, 同时传递参数
SuperType.call(this, 'jiegiser')
// 实例属性
this.age = 18
}
var instance = new SubType()
console.log(instance.name) // jieigiser
console.log(instance.age) // 18
# 借用构造函数存在的问题
如果仅仅是借用构造函数,也是无法避免构造函数模式存在的问题,方法都在构造函数中定义,因此函数复用就无从谈起,而且,在超类型的原型中定义的方法,对于类型而言不是不可见的,结果所有类型都只能使用构造函数模式
# 组合继承(最常用)
将原型链和借用构造函数的技术组合在一起,实现思路:使用原型链实现对原型属性和方法的继承,而通过借用构造函数,来实现对实例属性的继承,这样,即通过在原型上定义方法实现了函数复用,又能够保证每个实例都有让他自己的属性,比如下面的例子:
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name)
this.age = age
}
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
console.log(this.age)
}
var instance1 = new SubType('jiegiser', 18)
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "blue", "green", "black"]
instance1.sayName() // jiegiser
instance1.sayAge() // 18
var instance2 = new SubType('heli', 18)
console.log(instance2.colors) // ["red", "blue", "green"]
instance2.sayName() // heli
instance2.sayAge() // 18
上面的方法是实现继承的最常用的方法,而且可以通过 instanceof 以及 isPrototypeOf() 方法能够识别基于组合继承创建的对象;
# 原型式继承
基本思想:
function object(o) {
// 创建临时性的构造函数,然后将传入的对象作为构造函数的原型,最后返回了这个临时类的一个新实例。从本质上讲,
// object() 对传入的其中的对象执行了一次浅复制。
function F() {}
F.prototype = o
return new F()
}
const person = {
name: 'jiegiser',
friend: ['zhang', 'gao', 'qi', 'cao']
}
const anotherPerson = object(person)
anotherPerson.name = 'jiouba'
anotherPerson.friend.push('tian')
const yetAnotherPrson = object(person)
yetAnotherPrson.name = 'hhh'
yetAnotherPrson.friend.push('wu')
console.log(person.friend) // ["zhang", "gao", "qi", "cao", "tian", "wu"]
ECMAScript5 通过新增 Object.create 方法规范化了原型式继承,这个方法接受两个参数,一个用作新对象原型和(可选的)一个为新对象定义额外属性的对象。 在传入一个参数的情况下与 object 方法的行为相同:
const person = {
name: 'jiegiser',
friend: ['zhang', 'gao', 'qi', 'cao']
}
const anotherPerosn = Object.create(person)
anotherPerosn.name = 'jieouba'
anotherPerosn.friend.push('long')
const yetAnotherPrson = Object.create(person)
yetAnotherPrson.name = 'jiegiser'
yetAnotherPrson.friend.push('barbie')
console.log(person.friend) // ["zhang", "gao", "qi", "cao", "long", "barbie"]
Object.create()方法的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。如下面的代码:
const person = {
name: 'jiegiser',
friend: ['zhang', 'gao', 'qi', 'cao']
}
const anotherPerosn = Object.create(person, {
name: {
value: 'jieouba'
}
})
console.log(anotherPerosn.name) // jieouba
console.log(person.name) // jiegiser
# 寄生式继承
实现思路:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式,来增强对象,最后再像真地是他做了所有工作一样返回对象。比如下面的代码:
function object(o) {
// 创建临时性的构造函数,然后将传入的对象作为构造函数的原型,最后返回了这个临时类的一个新实例。从本质上将,
// object() 对传入的其中的对象执行了一次浅复制。
function F() {}
F.prototype = o
return new F()
}
function createAnother(original){
// 通过调用函数创建一个新对象
let clone = object(original)
// 以某种方式来增强这个对象
clone.sayHi = function() {
console.log('hi')
}
return clone // 返回这个对象
}
const perosn = {
name: 'jiegiser',
friend: ['zhang', 'gao', 'qi', 'cao']
}
// anotherPerosn 不仅具有 person 的所有属性和方法,而且还有自己的 sayHi 方法。
const anotherPerosn = createAnother(perosn)
console.log(anotherPerosn) // F {sayHi: ƒ}
anotherPerosn.sayHi() // hi
这种模式的使用场景就是,主要考虑对象而不是自定义类型和构造函数的情况下。使用这种模式;这里需要注意的是前面使用 object 方法不是必须的,任何能够返回新对象的函数都适用于此模式。
# 寄生组合式继承
组合继承虽然是常用的模式,但存在缺陷,就是无论什么情况,都会调用两次超类型构造函数:
- 一次是在创建子类型原型的时候
- 另一次是在子类型构造函数内部
子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性;比如下面的例子:
function SuperType(name) {
this.name = name
this.colors = ['red','green','blue']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name) // 第二次调用 SuperType
this.age = age
}
// 继承方法
SubType.prototype = new SuperType() // 第一次调用 SuperType
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
console.log(this.age)
}
在第一次调用的时候,SubType.prototype 会得到两个属性:name 和 colors,他们都是 SuperType 的实例属性(此时 name 为 undefined,colors 为 array)只不过现在位于 SubType 的原型中,当调用 SubType 构造函数时,又会在调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。最终会有两组 name 和 colors 属性,一组在实例上,一组在 SubType 原型中。
可以使用寄生组合式继承来解决这个问题, 所谓寄生组合式继承通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,其背后的基本思想是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已,本质上,就是使用寄生式继承
来继承超类型的原型,然后再将结果指定给子类型的原型。
寄生组合式继承的基本模式如下模式:
function inheritPrototype(subType, superType) {
// 第一步:创建超类型原型的一个副本,
let prototype = object(superType.prototype) // 创建对象
// 第二步:为创建的副本添加 constructor 属性,使得继承子类型构造函数的方法以及属性
// 弥补因重写原型为失去的的默认的 constructor 属性
// 增强对象
prototype.constructor = subType
// 第三步:将新创建的对象(也就是副本)赋值给予子类型的原型,
// 指定对象
subType.prototype = prototype
}
inheritPrototype 实现了寄生组合式继承的最简单的形式,他接收两个参数,子类型构造函数和超类型构造函数。
这样可以通过 inheritPrototype 函数,替换前面例子中为子类型原型赋值的语句了。
function SuperType(name) {
this.name = name
this.color = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
// 少了将 SubType.prototype = new SuperType() 以及 SubType.prototype.constructor = SubType
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function() {
console.log(this.age)
}
console.log(SubType) // SubType(name, age) { SuperType.call(this, name) this.age = age }
console.log(SuperType) // SuperType(name) { this.name = name this.color = ['red', 'blue', 'green'] }
const instance = new SubType('jiegiser', 18)
console.log(instance) // SubType { name: "jiegiser", color: Array(3), age: 18 }
instance.color.push('green') // ["red", "blue", "green", "green"]
console.log(instance.color)
const instance2 = new SubType('zhangsan', 18)
console.log(instance2.color) // ["red", "blue", "green"]
上面的高效体现在它之调用了一次 supertype 构造函数,因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变,因此,还能够正常使用 instanceof 以及 isProrotypeOf 方法。这个开发模式,普遍认为是引用类型最理想的继承。