欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript面向对象程序设计——继承

程序员文章站 2022-06-15 13:48:42
...

JavaScript面向对象程序设计——继承

简介

 面向对象程序设计的三个特点是封装性,继承性和多态性,上一篇 已经详述了封装性的实现方法,这一篇博客将会详述继承性的实现方法。

ES6中的继承

 ES6中通过语法糖实现了class关键字,能够很轻易的支持面向对象的特性。现在浏览器对于ES6的支持一般通过Babel翻译成ES5然后再交予浏览器执行,我们先来看看Babel文档中ES6实现继承的例子。

class SkinnedMesh extends THR EE.Mesh {
    constructor(geometry, materials) {
        super(geometry, materials);

        this.idMatrix = SkinnedMesh.defaultMatrix();
        this.bones = [];
        this.boneMatrices = [];
        //...
    }
    update(camera) {
        //...
        super.update();
    }
    static defaultMatrix() {
        return new THREE.Matrix4();
    }
}

语法已经很接近传统的面向对象语言了,稍微解释一下,extends关键字说明了SkinnedMesh类继承自TREE.Mesh类;constructor()是类的构造方法;super用于执行父类的方法;update()方法重写了父类的update()方法。所以通过ES6,封装性、继承性和多态性都能轻松实现。让我们来期待ES6特性被浏览器支持的那一天吧。接下来会介绍ES5中要实现继承性所要做的工作。

JavaScript中继承性的实现

 JavaScript中的继承跟封装一样,只有通过自己动手来实现传统面向对象语言中的特性。先来大概看一下Javascript实现继承性的集中方法:

  1. 使用原型链实现
  2. 借用构造函数
  3. 组合继承(伪经典继承)
  4. 原型式继承
  5. 寄生式继承

原型链实现继承

 使用原型链实现继承是一种朴素的方法,首先来看一下使用原型链的具体实现

function SuperType(){
    this.property=true;
}
SuperType.prototype.getSuperValue=function(){
    return this.property;
}
function SubType(){
    this.subproperty=false;
}
SubType.prototype=new SuperType();
SubType.prototype.getSubValue=function(){
    return this.subproperty;
}
var instance=new SubType();
console.log(instance.getSuperValue());//true
console.log(instance.getSubValue());//false

可以看到,Doctor继承了Person的方法,Doctor的实例person1能够调用PeronsgetPersonValue方法。说明继承成功。
那么问题来了,使用原型链实现继承有没有劣势呢,当然是有的,跟使用原型模式实现对象的构造一样,使用原型链不可避免的造成的引用类型的共享问题。还有一个问题是,不能在不影响所有对象实例的情况下,给父类型的构造函数传递参数。下面演示一下,引用类型共享的问题。

function SuperType(){
    this.colors=["red","blue","green"]
}
function SubType(){
}
SubType.prototype=new SuperType();
var instance1=new SubType();
instance1.colors.push("black");
console.log(instance1.colors);//["red","blue","green","black"]
var instance2=new SubType();
console.log(instance2.colors);//["red","blue","green","black"]

在示例中我们可以看到,type这个引用类型被所有的实例共享,不能实现单独的修改。
由于存在共享和不能给父构造函数传参的问题,所以原型链一半不会单独出现来作为实现继承的手段。

借用构造函数

 借用构造函数使用的原理是,函数也是一种对象。使用函数对象的apply()call()方法在新建的对象中只想构造方法。
具体实现如下所示

function SuperType(name){
    this.name=name;
    this.colors=["red","blue","green"]
}
function SubType(name){
    SuperType.call(this,name)
}
var instance1=new SubType("Jerry");
instance1.colors.push("black");
console.log(instance1.colors);//["red","blue","green","black"]
console.log(instance1.name);//Jerry
var instance2=new SubType("Tony");
console.log(instance2.colors);//["red","blue","green"]
console.log(instance2.name);//Tony

通过借用构造函数很轻易就解决了使用原型链的共享和参数传递问题,但借用构造函数也有问题跟构造函数模式构造对象一样,存在这函数无法复用的问题,还有一个问题就是父类原型中的方法对于子类是不可见的,导致所有的类型只能使用构造函数模式构建。总的一句,借用构造函数这种方法的复用性较差,所以也很少单独使用。

组合继承(伪经典继承)

 从上面的两种方案来看,原型链过于共享,借用构造函数过于封闭,那么就可以像一种方法把两者合起来使用。这就是组合继承的基本思想。也就是说属性使用借用构造函数继承而方法使用原型链继承。具体实现如下

function SuperType(name){
    this.name=name;
    this.colors=["red","blue","green"];
}
SuperType.prototype.sayName=function(){
    console.log(this.name);
}
function SubType(name,age){
    //继承属性
    SuperType.call(this,name);
    this.age=age;
}

//继承方法
SubType.prototype=new SuperType();
SubType.prototype.constructor=SubType;
SubType.prototype.sayAge=function(){
    console.log(this.age);
}

var instance1=new SubType("Jerry",18);
instance1.colors.push("black");
console.log(instance1.colors);//["red","blue","green","black"]
instance1.sayName();//Jerry
instance1.sayAge();//18

var instance2=new SubType("Tony",19);
console.log(instance2.colors);//["red","blue","green"]
instance2.sayName();//Tony
instance2.sayAge();//19

可以看出来组合继承使用借用构造函数的方法来继承属性,使用原型链来继承方法,兼顾了属性的私有性质和方法的共享性质。可以说是一个很好的解决方案,所以组合继承是JavaScript中最常用的继承模式。

原型式继承

 原型式继承名字跟使用原型链很相似,但其实不是一样东西。原型式继承的想法是借助原型可以基于已有的对象创建新对象,同时不必创建自定义类型。具体的想法如下

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

object内,先穿件一个临时构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。具体的实现如下

var person={
    name:"Tony",
    friends:["Jerry","Sam","Tom"]
}
var person1=object(person);
person1.name="Jim";
person1.friends.push("Rob");
console.log(person1.name);//Jim

var person2=object(person);
person2.name="Tommy";
person2.friends.push("Jenny");
console.log(person2.name);//Tommy

console.log(person.friends);//["Jerry","Sam","Tom","Rob","Jenny"]

原型式继承在只需要一个对象跟另外一个对象保持类似的情况下有很好的效果。在ES5中原型式继承通过Object.create()得到规范。Object.create()可以接受两个参数,当只有一个参数的时候拿上面的person1举例子就是var person1=Object.create(person),有两个参数的时候,第二个参数就是为新对象指定属性的值,同样拿person1举例子就是

var person1=Object(person,{
    name:{
        value:"Jim"
    }
})

寄生式继承

 寄生式继承是原型式继承的一种变种,思路和寄生构造函数和工厂模式类似,先创建一个用于封装继承过程的函数,该函数在内部增强对象,具体的实现如下

function createAnthor(original){
    var clone=Object(original);
    clone.sayHi=function(){
        console.log("Hi");
    }
    return clone;
}

var person={
    name:"Tony",
    friends:["Jenny","Jerry","Tim"]
}

var person1=createAnthor(person);
person1.sayHi();//Hi

这种继承模式,跟工厂模式构造方法一样不能表明类型,所以只有在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承才有用。而且寄生式继承还有一个缺点就是不能复用方法,和构造函数方法类似。

寄生组合式继承

 前面说的组合式继承是最常用的继承模式,但并不是最完美的继承模式,不完美在继承的时候会调用两次父类的构造函数,一次在继承属性的时候,一次在继承方法的时候。这样做会导致的后果是,原来父类的属性在子类中存在两份,一份存在于实例中,一份存在与子类的原型中。

JavaScript面向对象程序设计——继承

在途中我们可以看到,实例中有namecolors并且在__proto__中也有namecolors属性。解决这个问题就需要用到接下来要将的寄生组合式继承。寄生组合式继承通过构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路就是不必为了指定子类型的原型而调用父类型的构造函数,只需要父类型的一个副本。本质上就是使用寄生式继承来继承父类型的原型,然后将结果指定给子类型的原型。核心的实现如下

function inheritPrototype(subType,superType){
    var prototype=Object(superType.prototype);// 创建对象
    prototype.constructor=subType;//增强对象
    subType.prototype=prototype;//指定对象
}

分析一下这个函数,首先是创建父类原型的一个副本,然后为创建的副本添加constructor属性,弥补因为重写原型而失去的默认constructor,接着将新创建的对象赋值给子类型的原型。
测试代码如下

function SuperType(name){
    this.name=name;
    this.colors=["red","blue","green"];
}
SuperType.prototype.sayName=function(){
    console.log(this.name);
}
function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge=function(){
    console.log(this.age);
}

var instance1=new SubType("Jerry",18);
instance1.colors.push("black");
console.log(instance1.colors);//["red","blue","green","black"]
instance1.sayName();//Jerry
instance1.sayAge();//18

var instance2=new SubType("Tony",19);
console.log(instance2.colors);//["red","blue","green"]
instance2.sayName();//Tony
instance2.sayAge();//19

JavaScript面向对象程序设计——继承

通过上图我们可以看出,使用寄生组合式继承子类原型中没有冗余的属性,而且原型链还保持不变。现在普遍认为寄生组合式继承是引用类型最理想的继承模式。

总结

 通过这篇文章,我们梳理了JavaScript中实现继承的集中方法,探讨了各种方法的优劣势,也得出了最理想的继承模式。JavaScript面向对象编程也介绍的差不多了。接下来可能会写关于JavaScript模块化的文章,希望看官大人们多多关注。

参考资料《JavaScript高级程序设计》