全面解析JavaScript之继承

277 阅读8分钟

什么是继承 ?

继承是:JavaScript允许我们在已有类的基础上创建新类的机制。它为子类提供了灵活性,可以重用父类的方法和属性,并且可以对这些功能进行拓展。

继承有几种方法 ?

  • 原型链继承
  • 构造函数继承
  • 组合式继承
  • 复制继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • es6的class继承

原型链继承

前情提要:关于原型与原型链可以读我的另一篇文章:原型与原型链

function Parent() {
    this.name = ”张三“;
    this.age = 18;
}
Parent.prototype.getName = function () {
    console.log(this.name+"今年"+this.age+"岁了");
}
function Child() {};
Child.prototype = new Parent();
let kid = new Child();
kid.getName() // 张三今年18岁了
console.log(kid.name) // 张三

特点

  • 简单,易于实现,且继承关系纯粹。

  • 可通过子类直接访问父类原形链上得属性和方法 缺点

  • 基于父类创建的所有子类共享父类的属性。此时其中一个子类改变父类中的属性,则其他子类中该属性也会相应发生变化

  • 在创建子类的实例时无法向父类传递参数

  • 无法实现多继承

  • 为子类增加原型对象上得属性和函数时,必须放在父类的实例挂载到子类的原型上后。

构造函数继承

实现思路:通过call方法改变父类的this指向为子类创建与父类一样的属性和方法。

function Parent(hadGirlFriend) {
    this.name = '张三';
    this.hadGirlFriend = hadGirlFriend;
    this.getMsgInfo = function () {
        console.log("姓名:"+this.name+";"+"有女朋友否:"+this.hadGirlFriend)
    }
}
Parent.prototype.money = ”1亿美元“
function Child(hadGirlFriend) {
    Parent.call(this, ...arguments);
}
let kid1 = new Child("没有");
let kid2 = new Child("有");
kid1.getMsgInfo() // 姓名:张三;有女朋友否:没有
kid2.getMsgInfo() // 姓名:张三;有女朋友否:有
console.log(kid1.money// undefined

优点

  • 解决了原型链继承中子类共享父类属性的问题。
  • 可以向父类传递参数
  • 可以实现多继承 缺点
  • 实例只是子类的实例,并不是父类的实例(可通过instanceof进行验证)
  • 只能继承父类实例的属性和方法,并不能继承原型对象上得属性和方法。
  • 无法复用父类的实例函数。由于父类的实例函数将通过call函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例函数的引用,这会造成不必要的内存消耗,影响性能。

组合式继承

结合前两两种方式都各有不同,且缺点方向具有互补性。所以就有了组合式继承的方式。 实现思路:同时实现原型链继承和构造函数继承。

function Parent(age) {
    this.name = "李四";
    this.age = age || 18;
    this.getInfo = function () {
        console.log("姓名:"+this.name+";"+"年龄:"+this.age+";"+"有女朋友否:"+this.hadGirlFriend)
    }
}
Parent.prototype.hadGirlFriend = "有";
function Child() {
    Parent.call(this, ...arguments);
}
// Child.prototype = new Parent();
// 以上这种写法会导致每生成一个子类,父类就会执行两次。在改写父类的原型时,由于父类实例的优先级高于原型的优先级,所以当实例和原型存在同一属性或方法时,实例的会覆盖原型对象上得属性。
Child.prototype = Parent.prototype;
let kid1 = new Child(25);
kid1.getInfo(); // 姓名:李四;年龄:25;有女朋友否:有

优点

  • 既继承父类实例的属性和方法,又继承了父类原型对象上的属性和方法。
  • 实例化子类后的对象既是子类的实例,也是父类的实例。
  • 可以向父类中传递参数 缺点
  • 重复调用父类构造函数
  • 重复生成属性,造成不必要的内存浪费

复制继承

实现思路:先生成父类的实例,再通过for...in遍历父类实例的属性和方法。再将其依次赋值给子类的实例的属性和方法或者原型对象上的属性和方法。

function Parent(age) {
    this.name = "张三";
    this.age = age || 18;
}
Parent.prototype.getMsg = function() {
    console.log("姓名:"+this.name+";"+"年龄:"+this.age);
}
function Child() {
    let kid = new Parent(...arguments);
    for (let key in kid) {
        if (kid.hasOwnProperty(key)) {
            this[key] = kid[key];
        } else {
            Child.prototype[key] = kid[key];
        }
    }
}
let kid1 = new Child(26)
kid1.getMsg(); // 姓名:张三;年龄:26

优点

  • 支持多继承
  • 能同时继承父类实例的属性和方法以及父类实例原型上的实例和方法
  • 可以向父类的构造函数传值 缺点
  • 子类的实例只是子类的实例,不是父类的实例。
  • 每次生成子类的实例都会重复生成属性,造成不必要的内存浪费

原型式继承

实现思路:创建一个构造函数,构造函数的原型指向对象,然后用new创建实例并返回,本质上是对传入进来的对象进行一次浅拷贝。

function Parent(obj) {
    function Child() {};
    Child.prototype = obj;
    return new Child();
}
let obj = {
    name: "张三",
    age: 25,
    girlFriendNames: ['小红', '小兰'],
    getMsg: function() {
        console.log("姓名:"+this.name+";"+"年龄:"+this.age)
    },
    getAllGirlFriendNames: function() {
        console.log(this.girlFriendNames);
    }
}
let kid = new Parent(obj);
let kid1 = new Parent(obj);
kid.getMsg(); // 姓名:张三;年龄:25
kid1.getMsg(); // 姓名:张三;年龄:25
kid.name = "李四";
kid.girlFriendNames.push('小芬');
kid.getMsg(); // 姓名:李四;年龄:25
kid1.getMsg(); // 姓名:张三;年龄:25
kid.getAllGirlFriendNames(); //  ['小红', '小兰', '小芬']
kid1.getAllGirlFriendNames(); //  ['小红', '小兰', '小芬']

ES5通过Object.create方法规范化的原型式继承: 以下代码等价于上述代码:

let obj = {
    name: "张三",
    age: 25,
    girlFriendNames: ['小红', '小兰'],
    getMsg: function() {
        console.log("姓名:"+this.name+";"+"年龄:"+this.age)
    },
    getAllGirlFriendNames: function() {
        console.log(this.girlFriendNames);
    }
}
let kid = Object.create(obj);
let kid1 = Object.create(obj);
kid.getMsg(); // 姓名:张三;年龄:25
kid1.getMsg(); // 姓名:张三;年龄:25
kid.name = "李四";
kid.girlFriendNames.push('小芬');
kid.getMsg(); // 姓名:李四;年龄:25
kid1.getMsg(); // 姓名:张三;年龄:25
kid.getAllGirlFriendNames(); //  ['小红', '小兰', '小芬']
kid1.getAllGirlFriendNames(); //  ['小红', '小兰', '小芬']

优点

  • 减少了代码量

缺点

  • 生成的实例既不是子类的也不是父类的。
  • 引用的类型的值会共享。
  • 无法给父级构造函数传递参数。
  • 不支持多继承

寄生式继承

实现思路:在原型式继承的基础上再封装一层,来增强对象,只好将对象返回。

function Parent(obj) {
    let child = Object.create(obj);
    child.sayHi = function () {
        console.log('Hi');
    }
    return child;
}
let obj = {
    name: "张三",
    age: 25,
    girlFriendNames: ['小红', '小兰'],
    getMsg: function() {
        console.log("姓名:"+this.name+";"+"年龄:"+this.age)
    },
    getAllGirlFriendNames: function() {
        console.log(this.girlFriendNames);
    }
}
let kid1 = new Parent(obj);
let kid2 = new Parent(obj);
kid1.getMsg(); // 姓名:张三;年龄:25
kid2.getMsg(); // 姓名:张三;年龄:25
kid1.getAllGirlFriendNames(); // ['小红', '小兰']
kid2.getAllGirlFriendNames(); // ['小红', '小兰']
kid1.sayHi(); // Hi
kid2.sayHi(); // Hi
kid1.name = "李四";
kid1.girlFriendNames.push('小芬');
kid1.getMsg(); // 姓名:李四;年龄:25
kid2.getMsg(); // 姓名:张三;年龄:25
kid1.getAllGirlFriendNames(); // ['小红', '小兰', '小芬']
kid2.getAllGirlFriendNames(); // ['小红', '小兰', '小芬']

优点

  • 在不用创建构造函数的情况下,实现了原型链继承,并且代码量减少 缺点
  • 引用类型的值会共享
  • 无法给父级构造函数传参
  • 通过增强对象给子类实例增加属性或方法时,如果实例的属于与方法与原型上的冲突了,则实例会覆盖原型上同名属性或者方法。

寄生组合式继承

实现思路:利用构造函数来继承属性,通过原型链的混成形式来继承。不用指定子类再去调用父类的构造函数,只是需要父类原型的一个副本。本质上可以通过寄生式继承来继承父类的原型,然后再将结果返回给子类的原型。

function inheritPrototype(Child, Parent) {
    let prototype = Object.create(Parent.prototype);
    prototype.constructor = Child;
    Child.prototype = prototype;
}
function Parent(name) {
    this.name = name;
}
Parent.prototype.getName = function() {
    console.log(this.name);
}
function Child() {
    this.age = 18;
    Parent.call(this, ...arguments);
}
Child.prototype.getAge = function() {
    console.log(this.age);
}
inheritPrototype(Child, Parent);
let kid = new Child('王二麻子');
kid.getName(); // 王二麻子

总结:

  • 只调用了一次父类的构造函数,复制了一次父类的属性和方法。
  • 子类也可以使用父类原型上的属性和方法,是父类的原型方法是通过函数扩展的方式弄过来的,不会重复创建
  • 拥有了上述所有继承的优点,是目前引用类型最理想的继承方式
  • 因为切断了 Child.prototype 的通路,所以 Child 不能再自己的 prototype 上定义属性和方法了

ES6的class继承

实现思路:主要通过super和extends来实现class继承
实现原理: 寄生式组合继承

class Parent {
    constructor(name) {
        this.name = name;
    }
    getName() {
        console.log(this.name);
    }
}
class Child extends Parent{
    constructor(name) {
        super(name);
        this.age = 25;
    }
}
let kid = new Child('张三');
kid.getName(); // 张三
console.log(kid.age); // 25

extends: 用来创建一个类,该类是另一个类的子类。

作用

  • class可以通过extends关键字实现继承父类的所有属性和方法;
  • 若是用extends实现继承的子类内部没有constructor方法,则会被默认添加constructor和super

super:用于访问和调用一个对象的父对象上的函数,super关键字将单独出现,并且必须在使用this关键字之前使用

作用

  • 访问和调用父类上的属性和方法
  • 子类必须得在constructor中调用super方法,否则新建的实例就会报错。因为子类自己没有自己的this对象,而是继承父类的this对象。

总结

  • 在 ES5 中的继承,实质是先创造子类的实例对象 this,然后再将父类的属性和方法添加到 this 上(使用 Parent.call )
  • 在 ES6 中却不是这样的,它实质是先创造父类的实例对象 this(super),然后再用子类的构造函数去修改 this


**附加一道面试题:**
class A {}
class B extends A {}
const a = new A()
const b = new B()
a.proto ===
b.proto ===
B.proto ===
B.prototype.proto ===
b.proto.proto ===

console.log(a.__proto__ === A.prototype);
console.log(b.__proto__ === B.prototype);
console.log(B.__proto__ === A);
console.log(B.prototype.__proto__ === A.prototype);
console.log(b.__proto__.__proto__ === A.prototype);