js继承

又到了双休,但是手机老是给我提示暴雨预警,哪都不敢去。所以借着这个时机来复习一下js基础中的继承,顺便总结一下中间踩到的坑,每个实例的代码都全部写下来,虽然有很多是重复的但是这样方便查看和理解,如有错误欢迎指点^_^

原型链继承

原型链继承,就是让子类的原型属性指向父类的实例,这样子类没有在自己的实例里找到属性就会去原型上找(此时是父类的实例),再没有找到就去父类原型上找

// 创建父类
function Super() {
this.supArr = ['a', 'b']
this.name = 'super'
}
// 为父类原型添加方法
Super.prototype.sayName = function() {
console.log(this.name)
}
// 创建子类
function Sub() {
}
// 注意:为子类原型添加方法, 改变子类原型对象后获取不到此方法 ×
Sub.prototype.saySubName1 = function() {
console.log('saySubName1')
}
// 将子类的原型对象指向父类实例
Sub.prototype = new Super()
// 改变子类原型后,为子类原型添加方法 √
Sub.prototype.saySubName2 = function() {
console.log('saySubName2')
}
Sub.prototype.sayArr = function() {
console.log(this.supArr)
}

// 创建子类实例
let sub1 = new Sub()
let sub2 = new Sub()

sub1.name ='aaa'
sub2.name = 'bbb'
sub1.sayName() // aaa
sub2.sayName() // bbb
sub1.supArr.push('c')
sub2.sayArr() // ['a', 'b'] -> ['a', 'b', 'c']

使用原型链继承,可以帮助我们更加深刻的理解原型,但是也有很多缺点
缺点,1.修改父类引用类型,所有子类实例都会受到影响
2.不能实现多继承 3.不能向父类构造函数传参

构造函数继承

构造函数继承就是在子类的构造函数中通过call或apply方法,调用父类构造函数

function Super(name) {
this.name = name
this.supArr = ['a', 'b']
}
// 为父类原型添加方法,通过构造函数继承,子类并不能获取到父类的原型
// Super.prototype.sayName = function() {
// console.log(this.name)
// }
// Super.prototype.sayArr = function() {
// console.log(this.supArr)
// }
// 通过 call 或者 apply 调用父类构造函数
function Sub(name) {
Super.call(this, name)
}
Sub.prototype.sayName = function() {
console.log(this.name)
}
Sub.prototype.sayArr = function() {
console.log(this.supArr)
}
let sub1 = new Sub('zzj')
let sub2 = new Sub('sjj')
sub1.sayName() // zzj
sub2.sayName() // sjj
// 在 sub1里push c
sub1.supArr.push('c')
// sub2里的arr不会受到影响
sub2.sayArr() // ['a', 'b']

在这里本能的去父类原型上写方法,但是在调用的时候并获取不到父类的方法,
这是因为在子类通过call调用父类构造函数时,只是拷贝了父类中实例的属性
优点:1.可以向父类构造函数传参, 2. 引用类型不会受到其他实例的影响
缺点:1. 定义的方法不能复用,每个实例都会重新定义原型上的方法

组合继承

组合继承是结合原型链继承和构造函数继承,弥补了原型链继承不能传参和引用类型的改变会影响到其他实例,也弥补了构造函数继承只能在子类中定义方法,每个实例都会重新定义一次方法的缺点。

// 创建父类
function Super(name) {
this.supArr = ['a', 'b']
this.name = name
}
// 为父类原型添加方法
Super.prototype.sayName = function() {
console.log(this.name)
}

// 通过 call 或者 apply 调用父类构造函数
function Sub(name) {
// 第二次调用
Super.call(this, name)
}
// 将子类的原型对象指向父类实例
// 第一次调用
Sub.prototype = new Super()
Sub.prototype.sayArr = function() {
console.log(this.supArr)
}

let sub1 = new Sub('zzj')
let sub2 = new Sub('sjj')
sub1.sayName() // zzj
sub2.sayName() // sjj
// 在 sub1里push c
sub1.supArr.push('c')
// sub2里的arr不会受到影响
sub2.sayArr() // [ 'a', 'b' ]

虽然弥补了缺陷,但是子类调用构造函数时通过call拷贝了父类上的实例属性,然后子类又通过原型指向了父类的实例,那么子类原型上又有了父类上的实例属性。
优点: 1.结合了 原型链继承 和构造函数继承,弥补了各自的缺陷
缺点: 调用了两次父类构造函数,导致子类实例和原型上都有父类实例上的属性

原型式继承

原型式继承,就是基于已有对象,创建一个新对象和Object.create类似

function Object(sup) {
// 创建一个中间函数
function Fn() {}
// 将函数的原型指向sup
Fn.prototype = sup
// 返回函数的实例
return new Fn()
}

let person = {
name: 'super',
arr: ['a', 'b'],
sayName() {
console.log(this.name)
},
sayArr() {
console.log(this.arr)
}
};
let newObj = Object(person)
let newObj2 = Object(person)
newObj.name = 'newObj'
newObj2.name = 'newObj2'
newObj2.sayName() // newObj2
newObj.sayName() // newObj
// 修改 newObj里的数组会影响到其他实例
newObj.arr.push('c')
newObj2.sayArr() // [ 'a', 'b', 'c' ]

缺点和原型链继承一样

寄生组合继承

结合上面三个,将子类的原型指向通过Object函数创建的中间函数的原型,因为中间函数没有实例属性,这样子类原型上就不会有父类属性,这句话看代码就理解啦。中间我产生了一个疑问为什么不直接让子类原型指向父类原型呢?

// 原型式函数,创建一个中间函数,让中间函数的原型指向父类原型
function ObjectCreate(sup) {
function Fn() {}
Fn.prototype = sup.prototype;
return new Fn();
}
// 寄生函数,对子类操作的封装,主要是来弥补子类原型构造函数的缺失,和让子类构造函数指向中间函数
function inherit(sub, sup) {
var prototype = ObjectCreate(sup); // 创建对象
prototype.constructor = sub; // 增强对象
sub.prototype = prototype; // 指定对象
}
// 创建父类
function Super(name) {
this.supArr = ['a', 'b']
this.name = name
}
// 为父类原型添加方法
Super.prototype.sayName = function() {
console.log(this.name)
}

// 通过 call 或者 apply 调用父类构造函数
function Sub(name) {
Super.call(this, name)
}

// 注意:定义子类原型方法不能在 寄生组合继承操作之前, 因为子类的原型指向被改变了
// Sub.prototype.sayArr = function() {
// console.log(this.supArr)
// }

// 将子类的原型对象指向父类实例
// Sub.prototype = new Super()
// 不采用上面的方法,采用寄生原型式
inherit(Sub, Super)
// 疑问? 为什么不直接让子类原型直接指向父类原型呢
// 因为这样实例修改了或增加的原型上的属性,那么原型就改变了会影响到其他实例
// Sub.prototype = Super.prototype
// 定义子类原型方法应该在寄生组合继承操作之后
Sub.prototype.sayArr = function() {
console.log(this.supArr)
}

let sub1 = new Sub('zzj')
let sub2 = new Sub('sjj')

sub1.sayName() // zzj
sub2.sayName() // sjj
// 在 sub1里push c
sub1.supArr.push('c')
// console.log(sub1)
// console.log(sub2)
// sub2里的arr不会受到影响
sub1.sayArr()
sub2.sayArr() // [ 'a', 'b' ]

优点: 完美
缺点: 踩的坑有点多, 1.在Object函数中 应该是将父类原型赋值给 中间函数,而不是父类构造函数, 2. 定义子类原型方法应该在寄生组合操作之后

掌握到这些,那js的继承应该就差不多了,不过现在我更喜欢ES6的class中extend继承,但是这些理解了,有木有感觉自己又变强了^_^