通过栗子深入浅出js继承

自己整理了一下关于继承方面的知识,写上自己的一些体会,
通过举栗子的方式帮助大家理解,希望大家能有所收获。
文笔简陋,忘体谅....


一、什么是继承呢?

从字面上来看,继承可以理解为儿子继承了父亲的一些特征,
比如说儿子继承了父亲的交际能力,一些外貌特征,
但是儿子自己本身又具备一些他自己独特的特征,比如说他的交际能力更强于
父亲的交际能力,外貌特征方面跟父亲又有不同的地方。

那么在程序里面继承如何理解呢?

继承是指一个对象直接使用另一对象的属性和方法。
但是对象本身又可以具备自己的属性和方法。

那么如何理解多重继承呢?

一个儿子不仅仅可以继承父亲,他还可以继承母亲,这就是多重继承。
他会具备父亲跟母亲共同的特征。
在程序里面的意思就是说
一个对象可以继承多个对象,可以直接使用多个继承对象的属性和方法。


二、如何实现继承呢?

下面用5种方式实现继承:

  • 原型(prototype)实现继承
  • 父类实例实现继承
  • 利用空对象作为中介实现继承
  • call,apply实现多重继承
  • es6 extends 实现继承

1.原型(prototype)实现继承

看案例:

        /**
         * 父类
         * @param {[type]} name [description]
         * @param {[type]} age  [description]
         */
        function Father(name,age) {
             this.name=name;
             this.age=age;
             this.father='父亲';
        }
        /**
         * 父类的原型方法
         * @return {[type]} [description]
         */
        Father.prototype.sayHello=function(){
            alert('你好'+this.name);
        }
        // 实例化父亲这个类
        var fat=new Father('父亲',42);
        // 调用父类的方法,输出你好,父亲
        fat.sayHello();
        /**
         * 创建子类
         */
        function Child(name,age){
            this.name=name;
            this.age=age;
        }
        /**
         * 通过原型链实现继承 子类的原型等于父亲的原型
         * @type {[type]}
         */
        Child.prototype=Father.prototype;
        //实例化子类
        var chi=new Child('小钱',12);

        //子类的原型等于子类自己本身
        Child.prototype.constructor = Child;

        alert(chi.father);//调用父类的father属性,输出undefined
        chi.sayHello();//调用父类的sayHello方法,输出你好,小钱,覆盖了父类的name

重点在:

        /**
         * 通过原型链实现继承 子类的原型等于父类的原型
         * @type {[type]}
         */
        Child.prototype=Father.prototype;
    
        //实例化子类
        var chi=new Child('小钱',12);
        //子类的原型等于子自己本身
        Child.prototype.constructor = Child;

        alert(chi.father);//调用父类的father属性,输出undefined
        chi.sayHello();//调用父类的sayHello方法,输出你好,小钱,覆盖了父类的name

子类通过prototype继承了父类,
它相当于完全删除了prototype 对象原先的值,然后赋予一个新值

任何一个prototype对象都有一个constructor属性,指向它的构造函数。

所以目前子类的constructor属性是指向父类的constructor,
这显然会导致继承链的紊乱(明明是用构造函数Child生成的),因此我们必须手动纠正,将Child.prototype对象的constructor值改为Child


代码如下:

//子类的原型等于子类自己本身 
Child.prototype.constructor = Child;

这是很重要的一点,编程时务必要遵守

这边还存在一个问题,输出父类的father属性的时候,出现了undefined的问题

alert(chi.father);//调用父类的father属性,输出undefined

这是因为Child.prototype和Father.prototype现在指向了同一个对象,那么任何对Child.prototype的修改,都会反映到Father.prototype,也就是说目前Father的constructor也变成了Child,那么自然不存在father属性了,所以出现了undefined的问题,
那么我们下面看一下构造函数实现继承的方式。


2. 父类实例实现继承

看案例:

        /**
         * 父类
         * @param {[type]} name [description]
         * @param {[type]} age  [description]
         */
        function Father(name,age) {
             this.name=name;
             this.age=age;
             this.father='父亲';
        }
        /**
         * 父类的原型方法
         * @return {[type]} [description]
         */
        Father.prototype.sayHello=function(){
            alert('你好'+this.name);
        }
        
        
        // 实例化父亲这个类
        var fat=new Father('父亲',42);
        // 调用父类的方法,输出你好,父亲
        fat.sayHello();

        /**
         * 创建子类
         */
        function Child(name,age){
            this.name=name;
            this.age=age;
        }
        /**
         * 通过原型链实现继承 子类的原型等于父类的实例
         * @type {[type]}
         */
        Child.prototype=new Father();
        //实例化子类
        var chi=new Child('小钱',12);

        //子类的原型等于子类自己本身
        Child.prototype.constructor = Child;
        alert(chi.father);//调用父类的father属性,输出父亲
        chi.sayHello();//调用父类的sayHello方法,输出你好,小钱,覆盖了父类的name

与第一种方式不同的地方:

        /**
         * 通过原型链实现继承 子类的原型等于父类的实例
         * @type {[type]}
         */
        Child.prototype=new Father();
        
        //实例化子类
        var chi=new Child('小钱',12);

        //子类的原型等于子类自己本身
        Child.prototype.constructor = Child;
        alert(chi.father);//调用父类的father属性,输出父亲
        chi.sayHello();//调用父亲的sayHello方法,输出你好,小钱,覆盖了父类的name

这样就可以解决上面第一种直接用原型继承的问题,
通过父类的实例去实现继承,那么父类的constructor就不会发生变化,就不存在Child的prototype与Father的prototype同时一个指向的问题了。


3.利用空对象作为中介实现继承

看案例:

/**
         * [继承方法]
         * @param  {[type]} Child  [子类]
         * @param  {[type]} Parent [父类]
         * @return {[type]}        [description]
         */
        function extend(Child, Parent) {   
            //创建一个空的对象            
            var F = function() {}; 
            //空对象通过父类实例实现继承   
            F.prototype = new Parent(); 
            //子类的原型等于空对象的实例   
            Child.prototype = new F();  
            //改变子类的构造函数为自身  
            Child.prototype.constructor = Child;    
            Child.uber = Parent.prototype;  
        }

创建一个空对象最为中介实现继承,因为F是空对象,所以几乎不占内存。
调用方法如下:

 //调用继承方法
        extend(Child, Father);  
        //实例化子类
        var chi=new Child('小钱',12);
        alert(chi.father);//调用父类的father属性,输出父亲
        chi.sayHello();//调用父类的sayHello方法,输出你好,小钱,覆盖了父类的name

4.call,apply实现多重继承

看案例:

/**
         * 父类
         * @param {[type]} name [description]
         * @param {[type]} age  [description]
         */
        function Father(name,age) {
             this.name=name;
             this.age=age;
             this.father='父亲';
        }
        /**
         * 父类的原型方法
         * @return {[type]} [description]
         */
        Father.prototype.sayHello=function(){
            alert('你好'+this.name);
        }
        
        /**
         * 母亲类
         * @param {[type]} name [description]
         * @param {[type]} age  [description]
         */
        function Mother(name,age) {
             this.name=name;
             this.age=age;
             this.mother='母亲';
        }
        /**
         * 母亲类的原型方法
         * @return {[type]} [description]
         */
        Mother.prototype.sayGoodbye=function(){
            alert('再见'+this.name);
        }
        // 实例化父亲这个类
        var fat=new Father('父亲',42);
        // 实例化母亲这个类
        var mot=new Mother('母亲',41);

        // 调用父类的方法,输出你好,父亲
        fat.sayHello();

        // 调用母亲类的方法,输出再见,母亲
        mot.sayGoodbye();
        

        /**
         * 创建子类
         */
        function Child(name,age){
            this.name=name;
            this.age=age;
            /**
             * 使用apply实现继承,将父对象的构造函数绑定在子对象上
             * 绑定父类,绑定母类,实现多重继承
             */
            Father.apply(this,arguments);
            Mother.apply(this,arguments);
        }

        //实例化子类
        var chi=new Child('小钱',12);
        alert(chi.father);//调用父类的father属性,输出父亲
        alert(chi.mother);//调用母类的mother属性,输出母亲
        chi.sayHello();//调用父类的sayHello方法,输出你好,小钱,覆盖了父类的name
        chi.sayGoodbye();//调用母亲类的sayHello方法,输出再见,小钱,覆盖了母亲的name

看重点:

/**
         * 创建子类
         */
        function Child(name,age){
            this.name=name;
            this.age=age;
            /**
             * 使用apply实现继承,将父对象的构造函数绑定在子对象上
             * 绑定父类,绑定母类,实现多重继承
             */
            Father.apply(this,arguments);
            Mother.apply(this,arguments);
        }

在子类构造函数中,使用apply或call实现继承
那么子类就会同时具备2个父类的属性和方法

:call 跟 apply 的区别,本文暂不做介绍


5.es6 extends实现继承

看案例:

class Point {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
    }

    class ColorPoint extends Point {
        constructor(x, y, color) {
            this.color = color; // ReferenceError
            super(x, y);
            this.color = color; // 正确
        }
    }

Class之间可以通过extends
关键字实现继承,这比ES5的通过修改原型链实现继承方便很多。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

如果子类没有定义constructor方法,这个方法会被默认添加。

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

具体实现方式这边不做太多详细解释,想了解更多,可以去看看阮老师的es6入门


总结这4中方法的优缺点:

  1. 第一种直接继承prototype,即child.prototype = parent.prototype;
    优点:相比第二种效率更高,比较省内存;
    缺点:因为重新赋值了子类,如果子类的prototype对象上有属性或方法时,将被清除;
    且子对象的prototype对象修改后父对象的prototype也会被修改;

  2. 第二种:父类实例继承模式,即把子对象的prototype对象指向父类的一个实例;
    优点: 解决了第一种父类原型也会被修改的问题
    缺点:如果子对象的prototype对象上有属性或方法时,将被清除;
    注意:当改了prototype对象的constructor时,记得改回来,否则将造成继承链紊乱;

**一定要遵循这样的规则,即如果替换了prototype对象,那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。
**

o.prototype = {};
o.prototype.constructor = o;
  1. 第三种: 利用空对象作为中介,第一种第二种的结合体
    优点:因为空对象不占内存,相比第二种效率更高,比较省内存;
    缺点:如果子对象的prototype对象上有属性或方法时,将被清除;

  2. 第四种: 使用call(apply)把父对象的this指向改为子对象
    优点: 写法比较简便,很方便的可以实现多重继承

  3. 第五种: 使用es6 extends实现继承
    优点: 写法比较方法,结构比较清晰
    缺点: 浏览器兼容性还不是很好,需要babel编译成目前可以识别的es5


以上都是本人的一些总结,如果有体会不到位,错误的地方,烦请大家指出,谢谢

你可能感兴趣的:(通过栗子深入浅出js继承)