浅谈javascript中类的继承

写了一段时间的js后,发觉自己还停留在面向过程的编程方式中,而这种方式非常不利于团队协作开发,会带来大量的变量污染,当自己或者别人想重复使用已经写好一段时间的代码时会感到很混乱,并且也不利于他人去修改以这种方式写好的代码库,简而言之,影响团队代码的维护。 而解决这一问题的方法,则是使用面向对象的编程方式,在ES6之前,js中并没有很直观的继承这种机制,而是根据实际需求通过js的原型来实现不同样式的继承,而当我在初次接触js那多样的继承方式时直接懵逼了,故写下此文按照一种需求发展的方式来捋清这几种不同样式的继承。

一. 类式继承

//声明父类构造函数——动物
function Animal(nickName, age){
//共有属性(引用类型)
    this.action = {
        run: "the animal is running",
        eat: "the animal is eating"
    };
//共有属性(值类型)
    this.nickName = nickName || "defaultName";
    this.age = age || 0;
}
Animal.prototype.getNickName = function(){
    return this.nickName;
}
//声明子类构造函数——猫
function Cat(){
    this.voice = "meow";
}
Cat.prototype = new Animal();
Cat.prototype.say = function(){
    return this.voice;
}
var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);

console.log(myCat.nickName); //defaultName
console.log(yourCat.nickName); //defaultName

myCat.action.run = "my cat is runing";
console.log(myCat.action.run); //my cat is running
console.log(yourCat.action.run); //my cat is running

myCat.age = 5;
console.log(myCat.age); //5
console.log(yourCat.age); //0
console.log

从上面的例子可以看到,实现了基本的继承,但是有两个问题:

  1. 无法向父类传值,因而myCat和yourCat的nickName都是输出同样的默认值defaultName。
  2. 当其中一个子类实例myCat对父类Animal的引用类型进行修改后,影响到了另一个子类实例yourCat。可见两个子类的实例都是共享同一处父类的,因此如果需求是实例之间不能共享父类值那么类式继承的方式也是不能使用的。

另外这里还有一个需要注意的地方,当我们对myCat.age进行赋值的时候并没有影响到yourCat,同样类似myCat.action = “new”也是一样的结果,而当对Animal中的引用进行修改,例如myCat.action.run = “myCat is running”, 则是可以影响到父类的,从而导致yourCat.action.run也发生变化。这是因为js的机制造成,比如当对myCat.action.run进行修改时,js会先查找当前myCat是否包含action属性,发现没有,然后会去进行原型链的查询,同理,在读取值操作时在没有找到的情况下也会去进行原型链的查询,但是赋值操作如myCat.age=5, 这类则不会进行原型链的查询而是直接对myCat实例附加一个age属性,仔细想想这种机制在js执行效率以及适用性方面都会表现的很好,如果确实需要修改父类的age,那么可以通过myCat.__proto__.age = 5来进行修改。

上面这一大段废话,是我当时突然疑惑的地方,js基本功看来还是有点捉急呀( ⊙ o ⊙ )!如果您发现我的这个理解不太对或者太过表象,将非常感谢您能指正一下O(∩_∩)O

二.构造函数继承

通过构造函数继承的方式可以有效的解决类式继承中无法向父类传值的问题以及对父类引用类型的属性进行修改导致其他子类实例受到改动的问题。

//声明父类构造函数——动物
function Animal(nickName, age){
//共有属性(引用类型)
    this.action = {
        run: "the animal is running",
        eat: "the animal is eating"
    };
    this.test = [1,2,3,4];
//共有属性(值类型)
    this.nickName = nickName || "defaultName";
    this.age = age || 0;
}
Animal.prototype.getNickName = function(){
    return this.nickName;
}
//声明子类构造函数——猫
function Cat(nickName, age){
    Animal.apply(this, [nickName,age]);
    this.voice = "meow";
}

var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);
console.log(myCat.nickName); //Tom
console.log(yourCat.nickName); //Andy
console.log(myCat.getNickName()); //Error: myCat.getNickName is not a function

同样从上面的例子我们也能看出构造函数继承这种方式的问题:

  1. 没法获取到父类prototype中的方法或者属性,因此在使用getNickName方法的时候提示没有该方法。
  2. 而且这种方式当创建多个子类Cat的实例时,这些实例中关于父类构造函数的属性与方法都是各自单独的一份,因此当我们把本该共用的getNickName方法以this.getNickName的方式写入Animal构造函数中,那么子类Cat的多个实例都是各自拥有一个getNickName方法,因此没有达到代码复用的目的。

到这里,我们发现解决了一个问题但又冒出来了两个问题,更何况在类式继承中还有一个问题没有解决。

三.组合继承

为了解决在构造函数继承中无法获取到父类prototype的问题,只需要调整一下子类Cat的原型便可以了,即Cat.prototype = new Animal();

//声明父类——动物
function Animal(nickName, age){
//共有属性(引用类型)
    this.action = {
        run: "the animal is running",
        eat: "the animal is eating"
    };
    this.test = [1,2,3,4];
//共有属性(值类型)
    this.nickName = nickName || "defaultName";
    this.age = age || 0;
}
Animal.prototype.getNickName = function(){
    return this.nickName;
}
function Cat(nickName, age){
    Animal.apply(this, [nickName,age]);
    this.voice = "meow";
}
Cat.prototype = new Animal();
Cat.prototype.say = function(){
    return this.voice;
}

var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);
console.log(myCat.nickName); //Tom
console.log(yourCat.nickName); //Andy
console.log(myCat.getNickName()); //Tom

myCat.action.run = "myCat is running";
console.log(myCat.action.run); //myCat is running
console.log(yourCat.action.run); //the animal is running

如上例子所示,这种继承方式,解决了前面我们遇到的以下几个问题:

  1. 无法向父类传值。
  2. 各个实例之间共享父类。
  3. 可以获取到父类的原型上的属性和方法。

因为是组合了类式继承以及构造函数继承两个方式的继承方式,所以同样有构造函数继承方式中的第二个问题,即违背了代码的复用。其次,在子类Cat.prototype的时候执行了一次父类Animal构造函数,在实例化子类中又执行了一次父类Animal构造函数后,可见这是有一定开销的。所以虽然能解决功能使用的问题,但还不太令人满意。

四.原型式继承

原型式继承方式是道格拉斯提出来的一种实现继承的方法,当时提出这种方式为了解决什么样的问题,我不太了解,但根据上面在组合继承方式面临的开销问题,原型式继承中由于下面代码中object方法中的临时函数F是一个空函数,所以开销相对不大,先来看看道格拉斯他的这段功能性的代码:

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

要使用这个方法需要一个基对象作为参数传入。在该方法内部,有个临时的构造函数F,然后把传入的基对象作为该临时构造函数的原型,最后返回该临时函数的实例。
而在ES5中新增的Object.create()方法更为强大,可以作为该方法的替代品,而Object.create()方法的第二个参数对象上的任何属性都会作为新生成实例的属性:

var Animal = {
    nickName: "defaultName",
    actions: ["run", "eat", "drink"]
}

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

var animalA = object(Animal);
var animalB = Object.create(Animal);
animalA.actions.push("sleep");

console.log(animalA.nickName); //defaultName
console.log(animalA.actions); //["run", "eat", "drink", "sleep"]
console.log(animalB.actions); //["run", "eat", "drink", "sleep"]

var animalC = Object.create(Animal, {
    nickName:{
        value: "Jack"
    },
    actions: {
        value: ["walk"]
    }
});

console.log(animalC.nickName); //Jack
console.log(animalC.actions); //["walk"]
console.log(animalA.actions); //["run", "eat", "drink", "sleep"]
console.log(animalB.actions); //["run", "eat", "drink", "sleep"]

五.寄生式继承

寄生式继承其实就是对原型式继承再次封装,在这次封装方法中可以进行属性的添加,其实现效果和使用了Object.create()方法第二个参数后的效果是一致的,也就是也就是说这种继承方式完全可以被Object.create()方法代替,但是这种继承方式的思想确会被应用于等下会介绍的寄生组合式继承方式中,而寄生组合式继承将是我们最终比较完善的js继承方式。

var Animal = {
    nickName: "defaultName",
    actions: ["run", "eat", "drink"]
}

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

function createCat(obj){
    var o = object(obj);
    o.getName = function(){
        return this.nickName;
    }
    return o;
}

var myCat = createCat(Animal);
console.log(myCat.getName()); //defaultName

六.寄生组合式继承

先来看寄生组合式继承的核心代码,也就是根据寄生式继承的思想来实现的:

function inheritPrototype(subType, superType){
    var o = object(superType.prototype);
    o.constructor = subType;
    subType.prototype = o;
}

会传入两个参数,分别是子类构造函数subType,以及父类构造函数superType,然后通过原型式继承方式得到一个把父类构造函数的原型作为其原型的对象,即上述代码中的变量o,然后只需要让子类的原型指向这个o即可,如此便能解决组合继承方式中不能有效的复用共用函数的问题。但是若直接赋值o给子类原型后还会有一个问题,就是subType丢失了默认的constructor属性,于是subType在prototype中查询constructor时会找不到,从而进行原型链查找,于是就把superType的constructor属性作为subType的构造函数了,于是这改变了子类的构造函数,但是对于我们的继承功能的使用来讲这并没有什么大的影响,但是假如当我们写插件提供给别人的都是实例化后的对象比如myCat时,如果别人想扩展下我们的Cat对象,就可以通过myCat.constructor.prototype去修改或扩展我们的Cat类了。因此在把o赋值给subType的原型之前我们需要o.constructor = subType,添加一下constructor的指向为子类subType。

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

function inheritPrototype(subType, superType){
    var o = object(superType.prototype);
    o.constructor = subType;
    subType.prototype = o;
}

//声明父类——动物
function Animal(nickName, age){
//共有属性(引用类型)
    this.action = {
        run: "the animal is running",
        eat: "the animal is eating"
    };
//共有属性(值类型)
    this.nickName = nickName || "defaultName";
    this.age = age || 0;
}
Animal.prototype.getNickName = function(){
    return this.nickName;
}
function Cat(nickName, age){
    Animal.apply(this, [nickName,age]);
    this.voice = "meow";
    this.niubi = 0;
}

inheritPrototype(Cat, Animal);
Cat.prototype.say = function(){
    return this.voice;
}

var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);

myCat.action.run = "myCat is running";

console.log(myCat.action.run); // myCat is running
console.log(yourCat.action.run); // the animal is running

可见通过inheritPrototype方法我们实现了子类对父类的原型的继承,从而解决了多个实例无法共享使用同一段代码的代码不能复用的问题,并且使用inheritPrototye方法代替了组合式继承中的Cat.prototype = new Animal()来避免了多次调用父类构造函数的开销。显然,在子类Cat中使用了构造函数继承的思想。在利用了寄生式继承思想的inheritPrototye方法。配合上构造函数继承方式,也就形成了这个比较完善的寄生组合式继承。

当然js在面向对象的开发方式中,远不止这一点东西,这些都是一些很基础知识点,再此梳理一下,文中有些表达不准确或者理解的不到位或者不对的地方,还希望大家能指正一下O(∩_∩)O谢谢

    原文作者:sakisama
    原文地址: https://www.jianshu.com/p/dcc307ae2551
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞