一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第20天,点击查看活动详情。
继承的几种方式
说起继承,又是一个老生常谈的问题了。今天来讲讲继承的几种方法以及他们的优缺点吧。
源码地址:点击这里
一、原型链继承
原型链继承:通过原型将一个引用类型继承另一个引用类型的属性和方法。
// 构建一个父类
function Parants (){
this.name = 'zs',
this.age = 18
this.hubby = {
study: 'js',
read: '西游记'
}
}
// 定义方法
Parants.prototype.getHubby = function () {
console.log(this.name)
console.log(this.hubby)
}
// 构造子类
function Child() {}
// 子类的原型继承父类
Child.prototype = new Parent()
// 以子类原型创建一个新的实例
let Child1 = new Child()
Child1.hubby.song = 'like'
Child1.getHubby () // {study: 'js', read: '西游记', song: 'like'}
let Child2 = new Child()
Child2.getHubby () // {study: 'js', read: '西游记', song: 'like'}
// 可以看到这里 Child2,并没有去修改 Params的属性
// 但是打印数据不是原始数据,而是 Child1 修改之后的数据
总结优点:
- 父类的方法可以复用
缺点:
- 父类所有的
引用属性
(hubby),会被所有的子类共享,其中一个子类修改引用属性,其他的子类也会受到影响 - 子类型实例不能给父类型构造函数进行传参
二、借用构造函数继承
借用构造函数继承:在子类构造函数内部调用超类型构造函数。 通过使用 apply() 和 call() 方法可以在新创建的子类对象上执行构造函数。
// 构建父类
function Parants (){
this.hubby = {
study: 'js',
read: '西游记'
}
}
// 构建子类,使用 call 把父类的 this 指向子类
function Child() {
Parants.call(this)
}
// 利用子类创建一个新的实例
let child1 = new Child()
child1.hubby.song = 'like'
console.log(child1.hubby) // {study: 'js', read: '西游记', song: 'like'}
let child2 = new Child()
console.log(child2.hubby) // {study: 'js', read: '西游记'}
// 这里可以看到,就算child1 修改了属性,child2 也不会受到影响
这里使用 call()
或者是 apply()
的方法重新执行上下文,就相当于是这个新的 Child
实例对象上面重新执行了 Parant
构造函数中的所有代码,就相当于是每一个利用 Child
构造函数来创建的对象都有自己的 hubby
方法,实例之间互不影响。
关于传参
相比于原型链继承,借用构造函数有一个优点,那就是可以在子类构造函数中向父类构造函数进行传参。
// 构造父类
function Parant (name) {
// name:用来接收传过来的参数
this.info = {
// 将传来的参数赋值给一个属性
name: name
}
}
// 构造子类
function Child(name) {
// 继承父类,并且向父类进行传参
Parant.call(this, name)
}
// 创建新的实例,并传参
let child1 = new Child('zs')
// 打印 info
console.log(child1.info) // {name: 'zs'}
let child2 = new Child('ls')
// 打印 info
console.log(child2.info) // {name: 'ls'}
通过上面的例子,我们可以看到,在 Params
上面接收一个参数,并赋值给一个属性,然后在子类继承父类的构造函数时,由子类进行传参
,这样就可以实现不同的实例对象使用同一个构造函数,但内部属性不同。
总结优点:
- 可以在子类的构造函数中向父类进行传参
- 父类的引用属性不会在子类中共享
缺点:
- 子类不能访问父类原型上定义的方法(即不能访问
Parent.prototype
上定义的方法),因此所有方法属性都写在构造函数中,每次创建实例都会初始化
三、组合继承(原型链+借用构造函数)
组合继承:将原型链
和 借用构造函数
的技术组合在一块,从而发挥两者之长的一种继承模式。
// 构造父类
function Parant(age) {
this.age = age
this.name = ['jack', 'rose']
}
// 构造父类方法
Parant.prototype.sayAge = function() {
console.log(this.age)
}
// 构造子类
function Child(age, danger) {
// 将父类的 this 指向子类
Parant.call(this, age)
this.danger = danger
}
// 子类继承父类的原型
Child.prototype = new Parant()
// 构造子类的方法
Child.prototype.sayDanger = function (){
console.log(this.danger)
}
let child1 = new Child('19', '男')
child1.name.push('tom')
console.log(child1.name) // ['jack', 'rose', 'tom']
child1.sayDanger() // 男
child1.sayAge() // 19
let child2 = new Child('22', '女')
child2.name.push('lucy')
console.log(child2.name) // ['jack', 'rose', 'lucy']
child2.sayDanger() // 女
child2.sayAge() // 22
通过上面的例子,我们可以看到,Parant
构造函数上设置了 age
和 name
两个属性,然后又在原型上添加了一个 sayAge
的方法。Child
构造函数通过使用 call
继承了 Parant
构造函数的属性,传递了一个 danger
参数,同时给自身的原型上添加 sayDanger
的方法,这样创建的两个实例,既有自己的属性,包括继承的父类的 name
,同时也拥有父类的 sayDanger
方法。
总结优点:
- 父类的方法可以复用
- 可以在Child构造函数中向Parent构造函数中传参
- 父类构造函数中的引用属性不会被共享
缺点:
- 需要执行两次父类构造函数:
- 第一次
是Child.prototype = new Parent()
- 第二次是
Parant.call(this, age)
造成不必要的浪费
- 第一次
四、型式继承
型式继承:借助原型可以基于已有的对象创建新对象,同时还不必须因此创建自定义的类型。
// 创建一个构造函数,将接收到的参数赋值到原型上
function NewFn(obj) {
return NewFn.prototype = obj
}
// 创建一个对象
let person = {
name: 'zs',
age: 18,
friends: ['jack', 'tom', 'rose'],
sayName:function() {
console.log(this.name);
}
}
// 构造实例1
let person1 = NewFn(person)
person1.name = 'ls'
person1.friends.push('lucy')
person1.sayName() // ls
console.log(person1.friends) // ['jack', 'tom', 'rose', 'lucy']
// 构造实例2
let person2 = NewFn(person)
person2.name = 'ww'
person2.sayName() // ww
console.log(person2.friends) // ['jack', 'tom', 'rose', 'lucy']
通过上面的例子,我们可以看到,这种继承方式主要是通过一个对象,fn()
对传入其中的对象执行了一次浅复制
,将构造函数 NewFn
的原型直接指向传入的对象。
基本使用起来和第一种的原型链继承差不多。
总结优点:
- 父类的方法可以复用
缺点
- 父类所有的
引用属性
(friends),会被所有的子类共享,其中一个子类修改引用属性,其他的子类也会受到影响 - 子类型实例不能给父类型构造函数进行传参
五、寄生式继承
寄生式继承:寄生式继承是原型式继承的加强版
。创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正是它做了所有工作一样返回对象。
// 创建一个构造函数,将接收到的参数赋值到原型上
function NewFn(obj) {
return NewFn.prototype = obj
}
// 再创建一个函数,调用上面的构造函数
function createObj (original) {
let clone = fn(original)
// 用一个变量接收,添加一个方法
clone.sayName = function() {
console.log(this.name)
}
// 最后把这个结果返回
return clone
}
let person = {
name: 'zs',
age: 18,
friends: ['jack', 'tom', 'rose']
}
// 构造实例
let person1 = createObj(person)
person1.friends.push("lucy")
console.log(person1.friends) // ['jack', 'tom', 'rose', 'lucy']
person1.sayName() // zs
let person2 = createObj(person)
console.log(person2.friends) // ['jack', 'tom', 'rose', 'lucy']
通过上面的例子,我们可以看出,基本实现的效果和型式继承差不多,person1 修改了 ,person2 调用父类的引用数据也会随之发生改变。
总结缺点:
缺点(同原型式继承):
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
六、组合式寄生继承
组合式寄生继承:通过借用函数来继承属性,通过寄生式继承来继承方法。
function NewFn(obj) {
return NewFn.prototype = obj
}
// 构造父类
function Parant(age) {
this.age = age
this.name = ['jack', 'rose']
}
// 构造父类方法
Parant.prototype.sayAge = function() {
console.log(this.age)
}
// 构造子类
function Child(age, danger) {
// 将父类的 this 指向子类
Parant.call(this, age)
this.danger = danger
}
// 子类继承父类的原型
Child.prototype = new Parant()
function inheritPrototype(child, parent) {
let prototype = NewFn(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
Child.prototype = prototype; // 赋值对象
}
inheritPrototype(Child, Parant);
// 构造子类的方法
Child.prototype.sayDanger = function (){
console.log(this.danger)
}
let child1 = new Child("男", 23);
child1.sayAge(); // 23
child1.sayDanger(); // 男
child1.name.push("jack");
console.log(child1.name); // ['jack', 'rose', 'jack']
let child2 = new Child("女", 18);
child2.sayAge(); // 18
child2.sayDanger(); // 女
console.log(child2.name); // ['jack', 'rose']
总结优点:
- 只调用一次父类构造函数
- Child可以向Parent传参
- 父类方法可以复用
- 父类的引用属性不会被共享
寄生式组合继承可以算是引用类型继承的最佳模式
七、ES6 的 class 继承
1、使用 class 构造一个父类
class Parent {
constructor(name,age){
this.name = name
this.age = age
}
sayName(){
console.log(this.name);
}
}
2、使用 class 构造一个子类,并使用 extends 实现继承,super 指向父类的原型对象
class Child extends Parent{
constructor(name,age,gender){
super(name,age)
this.gender = gender
}
sayGender(){
console.log(this.gender);
}
}
3、实例化对象
const person1 = new Child('zs',18,'男')
person1.sayGender() // 男
person1.sayName() // zs
console.log(person1.name); // zs
console.log(person1.age); // 18
const person2 = new Child('ls',28,'女')
person2.sayGender() // 女
person2.sayName() // ls
console.log(person2.name); // ls
console.log(person2.age);// 28