js继承(总结向)

122 阅读5分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

前言

之前在面试过程中一直被问到继承😭,自己也总是答的不好,所以有时候因为这里会吃个大亏,因此此次来做一次总结,总结一下js的继承。

准备工作

首先是准备一个简单的父类

function Father(name){
    // 属性
    this.name = name || 'father'
    // 实例方法
    this.getName = function(){
        console.log('实例方法名,打印名字为:'+this.name);
    }
    // 实例引用属性
    this.arr = [1,2,3] 
}

Father.prototype.hasName = function(){
    console.log('原型链方法名'+this.name);
}

Father.prototype.age = 10

原型链继承

他的核心原理就是将子类的原型指向父类的实例

// 原型链继承
function Child1 (name){
    // this.name = name || 'child1'
}
// Child1.prototype.play = function() {  // 放在前面抛出错误,因为后面的prototype已经是new Father了, 放后面才能正常进行
//     console.log(this.name + '正在玩!');
// };
Child1.prototype = new Father()
Child1.name = 'child1'
Child1.prototype.play = function() {
    console.log(this.name + '正在玩!');
};


// 测试代码
let child11 = new Child1('child11') // 参数无效
let child12 = new Child1('child12')
Child1.name = 'C1'
child11.arr.push(4)
console.log(child11 instanceof Child1); // true
console.log(child11 instanceof Father); // true
child11.play() // play方法在前面定义抛出错误,后面则不会
child11.getName() // 实例方法名,打印名字为:father
child11.hasName() // 原型链方法名father
console.log(child11.name); // father 
console.log(child12.name); // father
console.log(child11.arr); // [ 1, 2, 3, 4 ]
console.log(child12.arr); // [ 1, 2, 3, 4 ]

优点:

  1. 写法简单

缺点:

  1. 不能实现多继承,因为子类的原型只能指向一个父类实例
  2. 无法向父类传参
  3. 父类上的引用数据类型为所用子类共享,一个子类实例改变,父类的引用属性也会变,其他子类的该属性也会改变
  4. 如果要为子类新增属性或者方法,只能在 new Father() 之后,并不能放在构造函数中,如上的代码示例,如果新增的方法放在改变子类原型的指向之前,改变指向之后新增的方法自然就没用了。

构造函数实现继承 (又叫借用构造函数继承)

其核心是通过call或者apply函数在字类中调用父类函数,并将this指向子类

// 借用构造函数继承
function Child (name){
    Father.call(this,name)
}
Child.prototype.play = function() {  
    console.log(this.name + '正在玩!');
};

// 测试代码
let child = new Child('child')
console.log(child.name); // child
console.log(child instanceof Father); // false
console.log(child instanceof Child); // true
child.play() // child正在玩!
child.getName() // 实例方法名,打印名字为:child
// child.hasName() // 不能访问父类原型的方法

优点:

  1. 可以实现多继承,只需在子类中多次调用构造函数即可
  2. 可以向父类传参,调用时传参即可
  3. 不同的对象实例拥有不同一份属性和方法,不会互相影响

缺点:

  1. 实例并不是父类的实例,由child instanceof Father为false可知
  2. 只能获取到父类上的属性和方法,父类原型上的属性和方法不能获取到
  3. 每次新建一个子类,都会调用一次父类,无法复用,性能较低

组合继承 (结合原型链继承和借用构造继承)

要点就是结合前面两种方式,这样子类实例也可以获取到父类原型上的方法和属性

function Child(name) {
  Father.call(this, name)
}
Child.prototype = new Father()
Child.prototype.construtor = Child
Child.prototype.play = function () {
  console.log(this.name + '正在玩!')
}

let child = new Child('child')
console.log(child.name) // child
child.hasName() // 原型链方法名child
console.log(child instanceof Father) // true

优点:

  1. 兼顾急用构造函数继承的优点
  2. 子类实例也是父类的实例,也能够访问到父类原型上的属性和方法

缺点:

  1. 调用了两次父类,子类拥有父类上相同的两套属性和方法
  2. 每次新建子类实例都会调用父类函数的缺点还未解决

原型式继承

用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。

function creat(o){
    function F(){}
    F.prototype = o
    return new F()
}

// 测试代码
let sub = new Father()
let child = creat(sub)
child.name = 'child'
child.arr.push(4)
console.log(child instanceof Father); // false
child.getName()
child.hasName()
console.log(sub.arr); // [1,2,3,4]

优点:

  1. 可以实现在拥有一个对象的情况,再在他的基础上创建一个新的对象
  2. 可以访问父类对象上的属性和方法

缺点:

  1. 与原型链继承一样,对父类的引用属性做修改时,其他该父类下的实例的该属性也会被修改
  2. 创建出来的实例并不是父类的实例
  3. 无法实现复用,属性和方法的添加只能在实例被创建出来后添加

寄生继承

通过原型继承的函数,再套一层来创建对象

function creat(o){
    function F(){}
    F.prototype = o
    return new F()
}

function creatobj(o){
    let obj = creat(o)
    // obj.name = 'child'
    return obj
}

// 测试代码
let sub = new Father()
let child = creatobj(sub)
child.name = 'child'
child.arr.push(4)
console.log(child instanceof Father); // false
child.getName() // 实例方法名,打印名字为:child
child.hasName() // 原型链方法名child
console.log(sub.arr); // [1,2,3,4]

优点:

与原型继承相似

缺点:

与原型继承相似

寄生组合继承

组合继承最大的缺点就是父类函数会被调用两次,所以会导致实例会有两组一模一样的属性和方法,通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点。

function Child(name) {
  Father.call(this, name)
}

(function () {
  function Super() {}
  Super.prototype = Father.prototype
  Child.prototype = new Super()
})()

let child = new Child('child')
let child2 = new Child('chil2')
console.log(child.name) // child
child.getName() // 实例方法名,打印名字为:child 
child.hasName() // 原型链方法名child
child.arr.push(4)
console.log(child2.arr) // [1,2,3]
console.log(child instanceof Father) // true
console.log(child instanceof Child); //true

这种算是比较完美的方案了,通过Super函数避免了重复调用两次父类的情况

后言

我们要掌握这么多种继承方法,光靠背肯定式不行的,重点是要理解js实现继承的思想

参考资料:

  • 《JavaScript高级程序设计第四版》