JS 实现继承的方式总结

140 阅读6分钟

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

1 什么是继承

子类通过继承,可以获得父类的方法和属性

2 继承的方法

2.1 原型链继承

核心:让父类实例作为子类实例的原型

Son.prototype = new Father()

于是son.__proto__Son.prototype

特点:

  • 实例可继承的属性
    1. 实例的构造函数的属性
    2. 父类构造函数属性
    3. 父类原型的属性

缺点:

  1. 实例无法向父类构造函数传参
  2. 只能单继承
  3. 所有实例会共享原型对象的所有属性

2.2 构造继承

核心:使用call或apply方法将父类构造函数引入子类构造函数

function Son() {
  // 相当于把父类构造函数中的代码复制过来执行
  Father.call(this, 'Jack')
  this.age = 12
}

特点:

  1. 继承父类构造函数的属性,不继承父类原型的属性
  2. 解决了原型链继承的缺点,即可传参、多继承、实例不会共享属性

缺点:

  1. 只能继承父类构造函数的属性
  2. 无法实现构造函数的复用(每次用每次都要重新调用)
  3. 每个新实例都有父类构造函数的副本,臃肿,影响子类的性能

2.3 组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

特点:

  1. 可以继承父类实例的属性和方法,也可以继承父类原型的属性和方法
  2. 可以不存在属性共享问题,如果不使用原型上的属性
  3. 可传参,函数可复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

2.4 实例继承(❌)

核心:为父类实例添加新特性,作为子类实例返回

function Cat(name) {
  var instance = new Animal()
  instance.name = name || 'Tom'
  return instance
}

特点:

  1. 不限制调用方式,不管是new Cat()还是Cat(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

2.5 原型式继承

核心:定义一个构造函数,让构造函数的原型对象指向已有对象o

function object(o) {
  var G = function() {}
  G.prototype = o
  return new G()
}
var obj = {
  name : 'ghostwu',
  age : 22,
  show : function(){
    return this.name + ',' + this.age;
  }
};
var obj2 = object( obj );
console.log( obj2.name, obj.age, obj.show() );

特点:

  1. 类似于复制一个对象,用函数来包装
  2. 本质是浅拷贝,以一个对象为模板复制出新的对象

缺点:

  1. 所有实例会共享原型上的属性
  2. 不能传递参数

es5中,新增了一个函数Object.create()实现了原型式继承

var obj = {
  skills : [ 'php', 'javascript' ]
};
var obj2 = Object.create( obj );
obj2.skills.push( 'python' );
var obj3 = Object.create( obj );
console.log( obj3.skills ); //php,javascript,python

2.6 寄生式继承

核心:把原型式继承再次封装,然后在对象上扩展新的方法

function object(o) {
  var G = function() {}
  G.prototype = o
  return new G()
}
function CreateObj(srcObj) {
  var dstObj = object(srcObj)
  dstObj.sayName = function() {
    return this.userName
  }
  return dstObj
}
var obj = {
  userName: 'Jack'
}
var obj2 = CreateObj(obj) 
console.log(obj2.sayName())

缺点:和原型式继承一样,存在被篡改的可能

2.7 寄生组合继承

核心:结合借用构造函数传递参数和寄生模式实现继承

function Cat(name) {
  Animal.call(this)
  this.name = name || 'Tom'
}
(function() {
  // 创建一个没有实例方法的类
  var Super = function() {}
  Super.prototype = Animal.prototype
  // 将实例作为子类的原型
  Cat.prototype = new Super()
})()
function inheritProtorype(subType, superType) {
    var prototype = Object.create(superType.prototype) // 创建对象,创建父类原型的一个副本
    prototype.constructor = subType // 增强对象,弥补因重写原型而失去的默认constructor属性
    subType.prototype = prototype // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
    alert(this.name)
}

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age) {
    SuperType.call(this, name)
    this.age = age
}

// 将父类原型指向子类
inheritProtorype(SubType, SuperType)

// 新增子类原型属性
SubType.prototype.sayAge = function() {
    alert(this.age)
}

var instance1 = new SubType('xyc', 23)
var instance2 = new SubType('lxh', 23)

instance1.colors.push('2')
instance2.colors.push('3')

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()

这是最成熟的方法,也是现在库实现的方法

2.8 混入方式继承多个对象

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do something
};

Object.assign会把OtherSuperClass原型上的函数拷贝到MyClass的原型上,使MyClass的所有实例都可用OtherSuperClass的方法。

2.9 es6 类继承extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下

// class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
// class中定义的所有方法是不可枚举的
// class中只能定义方法,不能定义对象,变量等
// class和方法内默认都是严格模式
// es5中constructor为隐式属性
class People{
  constructor(name='wang',age='27'){
    this.name = name;
    this.age = age;
  }
  eat(){
    console.log(`${this.name} ${this.age} eat food`)
  }
}
// 继承父类
class Woman extends People{ 
   constructor(name = 'ren',age = '27'){ 
     // 继承父类属性
     super(name, age); 
   } 
    eat(){ 
     // 继承父类方法
      super.eat() 
    } 
} 
let womanObj=new Woman('xiaoxiami'); 
womanObj.eat();

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    
    // Getter
    get area() {
        return this.calcArea()
    }
    
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle {

  constructor(length) {
    super(length, length);
    
    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 输出 100

总结

1、函数声明和类声明的区别

函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。

let p = new Rectangle(); 
// ReferenceError

class Rectangle {}
复制代码

2、ES5继承和ES6继承的区别

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
  • ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。