一步一步读懂JS继承模式

JavaScript做为一种弱类型编程语言被普遍使用于前端的各类技术中,因为JS中并无“类”的概念,因此js的OOP特性一直没有获得足够的重视,并且有至关一部分使用js的项目中采用的都是面向过程的编程方式。可是随着项目规模的不断扩大,代码量的不断增长,这种方式会让咱们编写不少重复的、无用的代码,并使得项目的扩展性、可读性、可维护性变得脆弱。所以,js的OOP编程技巧则成为进阶的一条必经之路。javascript

开始以前
因为js在ES6以前并无 “类” 的概念,所以咱们必需要了解这些特性(关系)。前端

  1. 在每使用function声明一个函数的时候,咱们称这个函数为构造函数,js都会为咱们自动建立一个原型对象。函数称这个对象叫作prototype(老公),原型对象称这个函数叫constructor(老婆)。
  2. 经过new关键字生成的对象就是这个函数的实例(Instance),这个实例称原型对象为__proto__(爸爸),同时也继承了原型对象的称呼constructor(孩儿他娘)。
  3. 实例可以继承原型对象自身和继承来的的全部属性以及方法,一样继承到的constructor属性指向构造函数。

至此,一个完整的家庭成员的关系已经构造出来了,并能够经过new关键字不断繁衍生息,后代老是能继承先辈的属性与方法。看如下这段代码:java

function SomeClass(value){
    this.value = value;
}
SomeClass.prototype.protoValu = 'prototype value';

var  Instance = new SomeClass('some value');
复制代码

这段代码经过new关键字实例化了一个对象Instance,这个对象继承了原型对象的protoValue属性,并拥有自身的value属性。那么实例化的new关键字到底起了什么做用呢?编程

  • 新建一个空对象o,并将函数的运行时上下文绑定为这个对象。(使得this指向o)
  • 使得o的__proto__指向SomeClass.prototype。(emmm应该是认爸爸)
  • 执行构造函数内容,给对象o添加属性与方法。(长出手和脚)
  • 判断构造函数是否有return语句,若是有执行return,若是没有则执行return o。(出生)

new关键字实际上起的做用就是,创造实例、繁衍后代的做用。设计模式

类式继承

function SupperClass(value){
    this.value = value;
    this.fn = function(){
        console.log(this.value);
    }
}
SupperClass.prototype.otherValue = 'other value';
//声明父类

function SubClass(value){
    this.subValue = value;
}
SubClass.prototype = new SupperClass("I'm supper value");
//声明子类,并使得子类继承自SupperClass
//以上为声明阶段

//经过如下方式使用
var Instance = new SubClass("I'm sub value");
console.log(Instance.value);
console.log(Instance.otherValue);
console.log(Instance.subValue);
Instance.fn();
复制代码

可是这种方式存在着一些问题:编程语言

  • 子类继承自父类的实例,而实例化父类的过程在声明阶段,所以在实际使用过程当中没法根据实际状况向父类穿参。所以,这种方式的的可扩展性不理想。
  • 子类的家庭关系不完善。Instance.constructor = SupperClass,由于SubClass并无constructor属性,因此最终会从SupperClass.prototype处继承获得该属性。
  • 不能为SubClass.prototype设置constructor属性,该属性会形成属性屏蔽,致使SubClass.prototype不能正确获取本身的constructor属性,毕竟SubClass.prototype实际上也是SupperClass的实例。

构造函数继承

function SupperClass(value1){
    this.xx = value1;
}
function SubClass(value1,value2){
    SupperClass.call(this,value1);
    this.xx = value2;
}

//实际使用
var Instance = new SubClass('value1','value2');
复制代码

构造函数继承方式的本质就是将父类的构造方法在子类的上下文环境运行一次,从而达到复制父类属性的目的,在这个过程当中并无构造出一条完整的原型链。函数

虽然构造函数继承解决了类式继承的不能实时向父类传参的问题,可是因为其没有一条完整的原型链,所以 子类不能继承父类的原型属性与原型方法 。我认为它只是一个实现了继承功能的一种方式,并不是真正的继承。性能

组合式继承--完美的继承方式

function SupperClass(value){
    this.value = value;
    this.fn = function(){
        console.log(this.value);
    }
}
SupperClass.prototype.otherValue = 'other value';
//声明父类

function SubClass(value1,value2){
    SupperClass.call(this,value1)
    this.subValue = value2;
}
SubClass.prototype = new SupperClass("I'm supper value");
//声明子类,并使得子类继承自SupperClass
//以上为声明阶段

//经过如下方式使用
var Instance = new SubClass("I'm supper value","I'm sub value");
复制代码

组合式继承集合了以上两种继承方式的优势,从而实现了“完美”继承全部属性并能动态传参的功能。可是这种方式仍然不能补齐子类的家庭成员关系,由于SubClass.prototype仍然是父类的实例。优化

另一点,相信你们也已经发现了,整个继承过程当中实际上调用了两次父类的构造方法,使得SubClass.prototype与Instance都有一份父类的自有属性/方法,这样会形成额外的性能开销,可是好在可以完整的实现继承的目的了。ui

原型式继承

原型式继承又被成为纯洁继承,它的重点只关注对象与对象之间的继承关系,淡化了类与构造函数的概念,这样能避免开发者花费过多的精力去维护类与类/类与原型之间的关系,从而将重心转移到开发业务逻辑上面来。

var supperObj = {
    key1: 'value',
    func: function(){
        console.log(this.key1);
    }
}

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

//实际使用方法
//var Instance = new Factory(supperObj);
var Instance = Factory(supperObj);
复制代码

原型式继承由于只关注与对象与对象之间的关系,所以大多数都是使用工厂函数的方法生成继承对象。在工厂函数中咱们 定义了一个中间函数(会被释放),并将这个函数的原型指向被继承的对象,所以经过这个函数生成的对象的__proto__也就指向了被继承对象。

在工厂函数内部实现继承的方式与类式继承实现的原理是同样的,区别在于原型式继承更加纯净,所以原型继承方式具备类式继承方式全部的缺点:

  • 没法根据使用的实际状况动态生成supperObj(没法动态传参)。
  • 虽然实现了对象的继承,可是生成的子类尚未添加本身的属性与方法。

同时原型继承也有如下优势:

  • 因为其纯洁性,开发者没必要再去维护constructor与prototype属性,仅仅只须要关注原型链。
  • 更少的内存开销。

寄生式继承--原型式继承的二次封装

在原型继承中,每执行一次工厂函数都会从新生成一个新的中间函数F,并在函数结束时被回收,像我这种强迫症患者是不太能接受这种方式的。所幸,ES5提供了Object.create(),而且在原型式继承,以及多继承中起着重要的做用。在寄生式继承中咱们会对原型继承作一次优化。

var supperObj = {
    key1: 'value',
    func: function(){
        console.log(this.key1);
    }
}
function inheritPrototype(obj,value){
    //var subObj = Factory(obj);
    var subObj = Object.create(obj);
    subObj.name = value;
    subObj.say = function(){
        console.log(this.name);
    }
    return subObj;
}

var Instance = inheritPrototype(supperObj,'sub');
Instance.func();
Instance.say();
复制代码

寄生式继承实际上就是对原型式继承的二次封装,在此次封装过程当中实现了根据提供的参数添加子类的自定义属性。可是缺点仍然存在,被继承对象没法动态生成

由于原型式继承是基于对象的继承,对象是没法接收参数的,所以要解决这个问题还要回到构造函数的问题上面来。

将类式继承与寄生式继承结合

function inheritPrototype(sub,sup){
    var obj = Object.create(sup.prototype);
    sub.prototype = obj;
    obj.constructor = sub;
    Object.defineProperty(obj,'constructor',{enumerable: false});
    //将constructor属性变为不可遍历,避免多继承时出现问题
}

function SupperClass(value1){
    this.supperValue = value1;
    this.func1 = function(){
        console.log(1);
    }
}
SupperClass.prototype.func2 = function(){
    console.log(this.supperValue);
}
//声明父类

function SubClass(value2){
    this.subValue = value2;
    this.func3 = function(){
        console.log(this.subValue);
    }
}
//声明子类

inheritPrototype(SubClass,SupperClass);
var Instance = new SubClass('sub');
console.log(Instance.supperValue);  //undefined
console.log(Instance.subValue); //sub
Instance.func1();   //Error
Instance.func2();   //undefined
Instance.func3();   //sub
复制代码

在这种方式中,因为obj对象并非SupperClass的实例,所以能够与SubClass维护一个完整的关系(prototype与constructor),在维护关系的同时 必定要修改constructor的可枚举属性

在维护了构造函数与原型之间的完整关系的同时,也有一个致命的缺陷----因为obj对象不是SupperClass的实例,因此在实例化子类的时候父类构造函数从未被调用过,所以 子类只能继承到父类原型属性与方法,没法继承到父类自有方法。

寄生组合继承

寄生组合继承就是将通过改良以后的寄生继承与构造函数继承方式组合,从而弥补寄生继承没法继承父类自有属性与方法的缺陷。

function SubClass(value1,value2){
    SupperClass.call(this,value1);
    this.subValue = value2;
    this.func3 = function(){
    	console.log(this.subValue);
    }
}
//声明子类

var Instance = new SubClass('sup','sub');
复制代码

组合以后,只用在SubClass中调用一次SupperClass的构造函数。本质上父类原型属性与原型方法是经过原型链来继承的,父类的自有方法是经过调用构造函数复制到自身实现继承的。

寄生组合继承不只完美的实现了属性与方法的继承,也避免了组合继承产生重复属性形成性能浪费,另外也支持建立子类时动态向父类传参。在大型项目中合理运用这种方式实现类的继承可以显著提高代码的可阅读性,以及可扩展性。

参考

《JavaScript设计模式》 《你不知道的JavaScript》

相关文章
相关标签/搜索