继承
原型链继承
原理: 实例能够访问原型(prototype)上的属性和方法。
本质: 父类(构造函数)重写 子类(构造函数)的原型(prototype),使得子类的实例有能力(通过原型链 --- 隐式原型)来父类的属性和方法。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.property = false;
}
SubType.prototype = new SuperType()
// 等价于下面两句
// SubType.prototype.property = new SuperType().property
// SubType.prototype.getSuperValue = new SuperType().getSuperValue
// 注意: 重新添加 SubType.prototype 属性的时候,我们不能使用对象字面量的形式。字面的形式是重新 SubType.prototyp。
var instance = new SubType()
// instance 实例能够通过隐式原型`__proto__`访问 SubType 原型上的属性和方法。
instance.property // false
instance.getSuperValue() // false
默认的原型
Object.prototype 是默认的原型,在默认的原型上挂载的属性和方法
- constructor
- hasOwnProperty
- isPrototypeOf
- propertyIsEnumberable
- toLocalString
- toString
- valueOf
最顶层的原型 null
Object 的顶层还有原型,就是 null。
存在的问题
- 引用数据类型的互相影响。
应用类型的数据在发生变化的时候,所有对齐引用的类型,都会发生变化。
- 参数传递的受阻。
我们不能传递参数给父构造函数,意味着原型链继承的灵活性很差。
借用构造函数
原理: 使用 call 方法改变 this 的指向问题.
子类构造函数的 this 通过 Function.prototype.call 方法改变指向,指向父类的构造。
call 的背后:通过在 this 的属性上添加一个父类方法,然后调用,调用之后将父类方法删除(也是如何实现一个call的本质)。
function SuperType () {
this.colors = ['red', 'blue', 'green']
}
function SubType () {
SuperType.call(this)
}
优点1:没有了引用类型干扰
这样实现的继承,解决了原型继承的第一个问题,引用类型数据的变化之后相互干扰。
原因: 每一个实例都是新的,深拷贝了:this.colors = ['red', 'blue', 'green']; 形成了一个单独的实例。
let ins1 = new SubType()
ins1.colors.push('black')
cnsole.log(ins1.colors) // ['red', 'blue', 'green', 'black']
let ins2 = new SubType()
cnsole.log(ins2.colors) // ['red', 'blue', 'green']
优点2:call 方法也能传递参数
function SuperType (name) {
this.name = name
}
function SubType () {
SuperType.call(this, "Mg-")
this.age = 27
}
var ins = new SubType()
console.log(ins.name) // Mg-
console.log(ins.age) // 27
问题
第一:函数的复用性,没有使用原型链,也是失去了复用性
第二:父类的原型的不可见性,子类根本不知道父类的哪些原型属性和方法。
组合继承
原理: 组合原型链 + 借用构造函数
function SuperType(name) {
this.name = name
this.color = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType() {
SuperType.call(this, name)
this.age = age
}
SubType.prototype.sayName = function () {
console.log(this.age)
}
SubType.prototype = new SuperType()
// 修该构造函数
SubType.prototype.constructor = SubType
console.log(SubType.prototype)
// 实例两个子类,没有了之前的缺点
const ins1 = new SubType('aaa', 29)
ins1.colors.push('black') // ['red', 'blue', 'green', 'black]
ins1.sayName() // aaa
ins1.sayAge() // 29
const ins2 = new SubType('bbb', 27)
console.log(ins2.color) // ['red', 'blue', 'green']
ins2.sayName() // bbb
ins2.sayAge() // 27
测试工具
- instanceof
- isPrototypeOf
问题
SuperType 构造函数会执行两次,第一次在子类构造函数内部,第二次在给子类原型赋值。
原型模式
ES5 规范化了原型式继承
- Obejct.create()
原理:
function create(o) {
function F(){}
F.prototype = o
return new F()
}
特点:创建出来的实例引用类型共享。这也是一个问题
var person = {
name: 'aaa',
firend: ['bbb', 'ccc', 'ddd']
}
var ano = Object.create(person)
ano.name = 'ttt='
ano.friends.push('one-two')
var oth = Object.create(person)
ano.name = 'ggg-'
ano.friends.push('three-four')
console.log(person.friends) // ['bbb', 'ccc', 'ddd', 'one-two', 'three-four']
Object.create 的第二个参数
var person = {
name: 'aaa',
firend: ['bbb', 'ccc', 'ddd']
}
// 相当于一个属性描述符
var ano = Object.create(person, {
name: {
value: 'Mag'
})
寄生式继承
特点:类似于工厂模式,又含有原型模式
// 原型模式
function create(o) {
function F(){}
F.prototype = o
return new F()
}
// 工厂函数中包含寄生的函数
function AnotherCreate(obj) {
var clone = create(obj) // {}, 挂载于原型上
clone.sayHi() {
console.log('hi')
}
return clone;
}
var person = {
name: 'aaa',
firend: ['bbb', 'ccc', 'ddd']
}
var a = AnotherCreate(person)
a.sayHi()
这种寄生式,扩展式的,是依附于别的对象的属性和方法,创建出来就有了别人的属性和方法。
特点 create 函数也不是必须的,任何能够返回一个对象的都可以。
组合继承模式
原理:实例继承实例的部分,原型继承原型的部分,分开继承,同时父类构造函数不在继承两次。
// 核心函数:
function inheritPrototype(SubType, SuperType) {
var prototype = create(Super.prototype)
prototype.constructor = SubType
SubType.prototype = prototype;
}
// create
function create(o) {
function F(){}
F.prototype = o
return new F()
}
// 实现一个继承
function Ass(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
Ass.prototype.sayName = function () {
console.log(this.name)
}
function Bcc(name, age) {
Ass.call(this, name)
this.age = age
}
inheritPrototype(Bcc, Ass)
Bcc.prototype.sayAge = function () {
console.log(this.age)
}
在 ES5 时代基本上就是使用这种寄生组合的方式来实现继承的问题。
回顾
-
ES5 中没有类的直接概念,ES6 才有了 class 的语法糖。有了更好的类的写法。
-
原型链继承,使用原型链继承是JS实现继承的底层知识,所以原型链时必须掌握的。但是引用类型的数据共享,传递参数的阻碍缺点有特特别明显
-
借用构造函数,使用函数的call方法,函数的call的特性,能够改变函数的调用时的运行环境,能够添加参数,和实例的引用类型是深拷贝。弥补了原型链继承的缺点,但是也失去了原型链,共享的特性。
-
组合继承模式,是将原型链和借用构造函数组合,取长补短,但是也有新的问题,就是父类构造函数的会执行两次。
-
原型式继承,es5 的 Object.create 方法就基于 一个对象创建另一个对象的这种原型式的继承方法,特点就是依赖于一个已经存在的对象,我们是依赖。但是缺点也是很明显,依赖一个存在的对象,另外,引用类型的数据(浅复制)的问题。
-
寄生式继承,结合了原型式继承和寄生模式,特点还是依附于一个对象,但是能够自己添加自己的方法。同原型式继承,没有构造函数,没有太多自定的东西。
-
寄生式组合继承,其实就结合和寄生模式和借用构造模式的结合,借用构造函数管理实例部分的继承,寄生式组合继承管理原型部分。