js继承的七种姿势

300 阅读5分钟

思维导图

js继承的七种姿势.png

一. js继承的思想

Brendan Eich(js的设计者)认为一种简易的脚本语言,其实是不需要"继承"机制的。但是JavaScript中一切皆对象,那就需要有一种机制,将对象联系起来,于是设计了"继承",通过new 构造函数创建对象,通过构造函数的prototype来实现数据共享。继承的本质则是通过原型链机制实现的扩展。

二. js继承的七种方式

1.原型链继承

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

function SuperType(name){
  this.name = name
  this.colors =['red','green']
}
SuperType.prototype.getSuperName = function(){
  console.log(this.name)
}

function SubType(name){
  this.name = name
}

// 重写 SubType.prototype对象
SubType.prototype = new SuperType("Super") 
SubType.prototype.getSubName = function(){
  console.log(this.name)
}

var sub1 = new SubType('Sub1')
var sub2 = new SubType('Sub2')
sub1.colors.push('yellow')
console.log(sub1,sub2)

图解

未命名表单 (1).png 从图中可以发现 实例对象sub1、sub2 的colors属性指向相同,改变一个会影响另外一个

截屏2021-07-20 下午5.20.36.png 缺陷:多个实例的引用类型属性指向相同,存在篡改的可能

2.借用构造函数继承

核心:使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

function SuperType(name){
  this.name = name
  this.colors =['red','green']
}
SuperType.prototype.getSuperName = function(){
  console.log(this.name)
}

function SubType(age){
  this.age = age
  SuperType.call(this)
}
SubType.prototype.getSubName = function(){
  console.log(this.name)
}

var sub1 = new SubType('Sub1')
var sub2 = new SubType('Sub2')
sub1.colors.push('yellow')
console.log(sub1,sub2)

图解

借用构造函数.png 借用构造函数继承的核心就在于SuperType.call(this),“借调”了SuperType构造函数,这样,SubType的每个实例都会将SuperType中的属性复制一份

缺陷:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能 优点:
  • 每一个实例属性都有父类的副本,解决了多个实例引用类型属性指向相同时被篡改的问题

3.组合继承

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

其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性

function SuperType(name){
  this.name = name
  this.colors =['red','green']
}
SuperType.prototype.getSuperName = function(){
  console.log(this.name)
}

function SubType(name,age){
  this.age = age
  SuperType.call(this,name) // 借用构造函数
}
SubType.prototype = new SuperType() // 原型链
SubType.prototype.constructor = SubType // 注意要修改constructor指向
SubType.prototype.getSubName = function(){
  console.log(this.name,this.age)
}
var sub1 = new SubType('Sub1',12)
var sub2 = new SubType('Sub2',13)
sub1.colors.push('yellow')
console.log(sub1,sub2)

图解

组合继承-第 3 页.png 缺陷: 父类中的实例属性和方法既存在于子类的实例中,又存在于子类的原型中,不过仅是内存占用,因此,在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法

这个方法是javascript中最常用的继承模式

优点:解决了借用构造函数,不能继承原型属性/方法

4.原型式继承

核心:直接将某个对象直接赋值给构造函数的原型

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}

object()对传入其中的对象执行了一次浅复制,将F的原型直接指向传入的对象,ES5中存在Object.create()的方法,能够代替上面的object方法

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

缺点:原型式继承多个实例的引用类型属性指向相同,存在篡改的可能

5.寄生式继承

核心:在原型式继承的基础上,增强对象,返回构造函数

function createAnother(original){ 
  var clone=object(original); // 过调用函数创建一个新对象
  clone.sayHi = function(){ // 以某种方式增强这个对象
    alert("hi");
  };
  return clone; // 返回对象
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

缺点:寄生式继承多个实例的引用类型属性指向相同,存在篡改的可能

6.寄生组合式继承

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

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); //创建对象
  prototype.constructor = subType;                    // 增强对象
  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;
}

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

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

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

寄生组合继承集合了前面几种继承优点,几乎避免了上面继承方式的所有缺陷,是执行效率最高也是应用面最广的,就是实现的过程相对繁琐

7.ES6中的extends

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

function _inherits(subType, superType) {
  
    // 创建对象,创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

优点

  • 语法简单易懂,操作更方便

三. 总结

  • ES6 Class extends是ES5继承的语法糖
  • JS的继承除了构造函数继承之外都基于原型链构建的
  • 可以用寄生组合继承实现ES6 Class extends,但是还是会有细微的差别

组合继承-第 4 页.png

四. 参考