美文网首页
JavaScript中的继承方式

JavaScript中的继承方式

作者: 一直玩编程 | 来源:发表于2016-04-06 17:15 被阅读114次

    基于这篇文章的一些名称约定:

    /*
     * 约定
     */
    function Fun(){
        // 私有属性
        var val = 1;        // 私有基本属性
        var arr = [1];      // 私有引用属性
        function fun(){}    // 私有函数(引用属性)
     
        // 实例属性
        this.val = 1;               // 实例基本属性
        this.arr = [1];             // 实例引用属性
        this.fun = function(){};    // 实例函数(引用属性)
    }
     
    // 原型属性
    Fun.prototype.val = 1;              // 原型基本属性
    Fun.prototype.arr = [1];            // 原型引用属性
    Fun.prototype.fun = function(){};   // 原型函数(引用属性)
    

    上面的约定应该是比较合理的,如果难以理解,可以查看黯羽轻扬:JS学习笔记2_面向对象,了解更多的基本常识

    js里常用的如下两种继承方式:

    • 原型链继承(对象间的继承)
    • 类式继承(构造函数间的继承)

    由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。所以,要想实现继承,可以用js的原型prototype机制或者用apply和call方法去实现

    在面向对象的语言中,我们使用类来创建一个自定义对象。然而js中所有事物都是对象,那么用什么办法来创建自定义对象呢?这就需要用到js的原型:

    我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模版(prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的__Proto__指针,指向原型对象)。

    js可以通过构造函数和原型的方式模拟实现类的功能。 另外,js类式继承的实现也是依靠原型链来实现的。

    原型式继承与类式继承

    类式继承是在子类型构造函数的内部调用超类型的构造函数。
    严格的类式继承并不是很常见,一般都是组合着用:

    function Super(){
        this.colors=["red","blue"];
    }
     
    function Sub(){
        Super.call(this);
    }
    

    原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链

    原型链继承

    为了让子类继承父类的属性(也包括方法),首先需要定义一个构造函数。然后,将父类的新实例赋值给构造函数的原型。代码如下:

    <script>
        function Parent(){
            this.name = 'mike';
        }
     
        function Child(){
            this.age = 12;
        }
        Child.prototype = new Parent();//Child继承Parent,通过原型,形成链条
     
        var test = new Child();
        alert(test.age);
        alert(test.name);//得到被继承的属性
        //继续原型链继承
        function Brother(){   //brother构造
            this.weight = 60;
        }
        Brother.prototype = new Child();//继续原型链继承
        var brother = new Brother();
        alert(brother.name);//继承了Parent和Child,弹出mike
        alert(brother.age);//弹出12
    </script>
    

    以上原型链继承还缺少一环,那就是Object,所有的构造函数都继承自Object。而继承Object是自动完成的,并不需要我们自己手动继承,那么他们的从属关系是怎样的呢?

    确定原型和实例的关系

    可以通过两种方式来确定原型和实例之间的关系。操作符instanceof和isPrototypeof()方法:

    alert(brother instanceof Object)//true
    alert(test instanceof Brother);//false,test 是brother的超类
    alert(brother instanceof Child);//true
    alert(brother instanceof Parent);//true
    

    只要是原型链中出现过的原型,都可以说是该原型链派生的实例的原型,因此,isPrototypeof()方法也会返回true

    在js中,被继承的函数称为超类型(父类,基类也行),继承的函数称为子类型(子类,派生类)。使用原型继承主要由两个问题:
    一是字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。

    伪类解决引用共享和超类型无法传参的问题,我们可以采用“借用构造函数”技术

    d借用构造函数(类式继承)

    <script>
        function Parent(age){
            this.name = ['mike','jack','smith'];
            this.age = age;
        }
     
        function Child(age){
            Parent.call(this,age);
        }
        var test = new Child(21);
        alert(test.age);//21
        alert(test.name);//mike,jack,smith
        test.name.push('bill');
        alert(test.name);//mike,jack,smith,bill
    </script>
    

    借用构造函数虽然解决了刚才两种问题,但没有原型,则复用无从谈起,所以我们需要原型链+借用构造函数的模式,这种模式称为组合继承

    组合继承

    <script>
        function Parent(age){
            this.name = ['mike','jack','smith'];
            this.age = age;
        }
        Parent.prototype.run = function () {
            return this.name  + ' are both' + this.age;
        };
        function Child(age){
            Parent.call(this,age);//对象冒充,给超类型传参
        }
        Child.prototype = new Parent();//原型链继承
        var test = new Child(21);//写new Parent(21)也行
        alert(test.run());//mike,jack,smith are both21
    </script>
    

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

    call()的用法:调用一个对象的一个方法,以另一个对象替换当前对象。

    call([thisObj[,arg1[, arg2[, [,.argN]]]]])
    

    原型式继承

    这种继承借助原型并基于已有的对象创建新对象,同时还不用创建自定义类型的方式称为原型式继承

    <script>
         function obj(o){
             function F(){}
             F.prototype = o;
             return new F();
         }
        var box = {
            name : 'trigkit4',
            arr : ['brother','sister','baba']
        };
        var b1 = obj(box);
        alert(b1.name);//trigkit4
     
        b1.name = 'mike';
        alert(b1.name);//mike
     
        alert(b1.arr);//brother,sister,baba
        b1.arr.push('parents');
        alert(b1.arr);//brother,sister,baba,parents
     
        var b2 = obj(box);
        alert(b2.name);//trigkit4
        alert(b2.arr);//brother,sister,baba,parents
    </script>
    

    原型式继承首先在obj()函数内部创建一个临时性的构造函数 ,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

    寄生式继承

    这种继承方式是把原型式+工厂模式结合起来,目的是为了封装创建的过程。

    <script>
        function create(o){
            var f= obj(o);
            f.run = function () {
                return this.arr;//同样,会共享引用
            };
            return f;
        }
    </script>
    

    组合式继承的小问题

    组合式继承是js最常用的继承模式,但组合继承的超类型在使用过程中会被调用两次;一次是创建子类型的时候,另一次是在子类型构造函数的内部

    <script>
        function Parent(name){
            this.name = name;
            this.arr = ['哥哥','妹妹','父母'];
        }
     
        Parent.prototype.run = function () {
            return this.name;
        };
     
        function Child(name,age){
            Parent.call(this,age);//第二次调用
            this.age = age;
        }
     
        Child.prototype = new Parent();//第一次调用
    </script>
    

    以上代码是之前的组合继承,那么寄生组合继承,解决了两次调用的问题。

    寄生组合式继承

    <script>
        function obj(o){
            function F(){}
            F.prototype = o;
            return new F();
        }
        function create(parent,test){
            var f = obj(parent.prototype);//创建对象
            f.constructor = test;//增强对象
        }
     
        function Parent(name){
            this.name = name;
            this.arr = ['brother','sister','parents'];
        }
     
        Parent.prototype.run = function () {
            return this.name;
        };
     
        function Child(name,age){
            Parent.call(this,name);
            this.age =age;
        }
     
        inheritPrototype(Parent,Child);//通过这里实现继承
     
        var test = new Child('trigkit4',21);
        test.arr.push('nephew');
        alert(test.arr);//
        alert(test.run());//只共享了方法
     
        var test2 = new Child('jack',22);
        alert(test2.arr);//引用问题解决
    </script>
    

    call和apply

    全局函数apply和call可以用来改变函数中this的指向,如下:
    
     // 定义一个全局函数
        function foo() {
            console.log(this.fruit);
        }
        
        // 定义一个全局变量
        var fruit = "apple";
        // 自定义一个对象
        var pack = {
            fruit: "orange"
        };
        
        // 等价于window.foo();
        foo.apply(window);  // "apple",此时this等于window
        // 此时foo中的this === pack
        foo.apply(pack);    // "orange"
    

    简单原型链

    这是实现继承最简单的方式了,真的超简单,核心就一句话(在代码中用注释标明了)

    1.具体实现

    function Super(){
        this.val = 1;
        this.arr = [1];
    }
    function Sub(){
        // ...
    }
    Sub.prototype = new Super();    // 核心
     
    var sub1 = new Sub();
    var sub2 = new Sub();
    sub1.val = 2;
    sub1.arr.push(2);
    alert(sub1.val);    // 2
    alert(sub2.val);    // 1
     
    alert(sub1.arr);    // 1, 2
    alert(sub2.arr);    // 1, 2
    

    2.核心

    拿父类实例来充当子类原型对象

    3.优缺点

    优点:
    1. 简单,易于实现
    缺点:
    1. 修改sub1.arr后sub2.arr也变了,因为来自原型对象的引用属性是所有实例共享的。可以这样理解:执行sub1.arr.push(2);先对sub1进行属性查找,找遍了实例属性(在本例中没有实例属性),没找到,就开始顺着原型链向上找,拿到了sub1的原型对象,一搜身,发现有arr属性。于是给arr末尾插入了2,所以sub2.arr也变了
    2. 创建子类实例时,无法向父类构造函数传参

    借用构造函数

    简单原型链真够简单,可是存在2个致命缺点简直不能用,于是上个世纪末的jsers就想办法fix这2个缺陷,然后出现了借用构造函数方式

    1. 具体实现

      function Super(val){
      this.val = val;
      this.arr = [1];

       this.fun = function(){
           // ...
       }
      

      }
      function Sub(val){
      Super.call(this, val); // 核心
      // ...
      }

      var sub1 = new Sub(1);
      var sub2 = new Sub(2);
      sub1.arr.push(2);
      alert(sub1.val); // 1
      alert(sub2.val); // 2

      alert(sub1.arr); // 1, 2
      alert(sub2.arr); // 1

      alert(sub1.fun === sub2.fun); // false

    2. 核心

    借父类的构造函数来增强子类实例,等于是把父类的实例属性复制了一份给子类实例装上了(完全没有用到原型)

    1. 优缺点

    优点:

    • 解决了子类实例共享父类引用属性的问题
    • 创建子类实例时,可以向父类构造函数传参
      P.S.前辈就这么高效,两个缺陷瞬间修复

    缺点:

    • 无法实现函数复用,每个子类实例都持有一个新的fun函数,太多了就会影响性能,内存爆炸。。
      P.S.好吧,刚修复了共享引用属性的问题,又出现了这个新问题。

    组合继承(最常用)

    目前我们的借用构造函数方式还是有问题(无法实现函数复用),没关系,接着修复,jsers吭哧吭哧又搞出了组合继承

    1. 具体实现

      function Super(){
      // 只在此处声明基本属性和引用属性
      this.val = 1;
      this.arr = [1];
      }
      // 在此处声明函数
      Super.prototype.fun1 = function(){};
      Super.prototype.fun2 = function(){};
      //Super.prototype.fun3...
      function Sub(){
      Super.call(this); // 核心
      // ...
      }
      Sub.prototype = new Super(); // 核心

      var sub1 = new Sub(1);
      var sub2 = new Sub(2);
      alert(sub1.fun === sub2.fun); // true

    2. 核心

    把实例函数都放在原型对象上,以实现函数复用。同时还要保留借用构造函数方式的优点,通过Super.call(this);继承父类的基本属性和引用属性并保留能传参的优点;通过Sub.prototype = new Super();继承父类函数,实现函数复用

    1. 优缺点

    优点:

    * 不存在引用属性共享问题
    * 可传参
    * 函数可复用
    缺点:

    * (一点小瑕疵)子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的。。。又是内存浪费,比刚才情况好点,不过确实是瑕疵
    P.S.如果无法理解这个”多余“,可以查看黯羽轻扬:JS学习笔记2_面向对象,文章末尾有更详细的解释.

    寄生组合继承(最佳方式)

    从名字就能看出又是对组合继承的优化,不是说组合继承有瑕疵吗,没关系,我们接着追求完美

    1. 具体实现

      function beget(obj){ // 生孩子函数 beget:龙beget龙,凤beget凤。
      var F = function(){};
      F.prototype = obj;
      return new F();
      }
      function Super(){
      // 只在此处声明基本属性和引用属性
      this.val = 1;
      this.arr = [1];
      }
      // 在此处声明函数
      Super.prototype.fun1 = function(){};
      Super.prototype.fun2 = function(){};
      //Super.prototype.fun3...
      function Sub(){
      Super.call(this); // 核心
      // ...
      }
      var proto = beget(Super.prototype); // 核心
      proto.constructor = Sub; // 核心
      Sub.prototype = proto; // 核心

      var sub = new Sub();
      alert(sub.val);
      alert(sub.arr);
      P.S.等等,生孩子函数是啥东西,怎么没听过?还有标明了核心的3句话,怎么没看明白?别着急,我们喝杯茶接着看

    2. 核心

    用beget(Super.prototype);切掉了原型对象上多余的那份父类实例属性

    P.S.啥?没看明白?哦哦~,忘记说原型式和寄生式继承了,就说怎么总觉得忘了锁门。。这记性

    P.S.寄生组合式继承,这名字不是很贴切,和寄生式继承关系并不是特别大

    1. 优缺点

    优点:完美了

    缺点:理论上没有了(如果用起来麻烦不算缺点的话。。)

    P.S.用起来麻烦是一方面,另一方面是因为寄生组合式继承出现的比较晚,是21世纪初的东西,大家等不起这么久,所以组合继承是最常用的,而这个理论上完美的方案却只是课本上的最佳方式了

    原型式

    其实介绍完上面的完美方案就可以结束了,但从组合继承到完美方案好像有一段不小的思维跳跃,有必要把故事说清楚

    1. 具体实现

    function beget(obj){   // 生孩子函数 beget:龙beget龙,凤beget凤。
        var F = function(){};
        F.prototype = obj;
        return new F();
    }
    function Super(){
        this.val = 1;
        this.arr = [1];
    }
     
    // 拿到父类对象
    var sup = new Super();
    // 生孩子
    var sub = beget(sup);   // 核心
    // 增强
    sub.attr1 = 1;
    sub.attr2 = 2;
    //sub.attr3...
     
    alert(sub.val);     // 1
    alert(sub.arr);     // 1
    alert(sub.attr1);   // 1
    

    P.S.诶~看到了没,生孩子函数beget出现了

    2. 核心

    用生孩子函数得到得到一个“纯洁”的新对象(“纯洁”是因为没有实例属性),再逐步增强之(填充实例属性)

    P.S.ES5提供了Object.create()函数,内部就是原型式继承,IE9+支持

    3. 优缺点

    优点:

    • 从已有对象衍生新对象,不需要创建自定义类型(更像是对象复制,而不是继承。。)
      缺点:

    • 原型引用属性会被所有实例共享,因为是用整个父类对象来充当了子类原型对象,所以这个缺陷无可避免
      无法实现代码复用(新对象是现取的,属性是现添的,都没用函数封装,怎么复用)
      P.S.这东西和继承有很大关系吗?为什么尼古拉斯把它也列为实现继承的一种方式?关系不大,但有一定关系

    6种继承方式的联系

    继承.jpg

    P.S.虚线表示辅助作用,实线表示决定性作用

    转自文章

    Web最新资讯,请关注我的微信公众号“一起玩前端”或扫描二维码关注.


    相关文章

      网友评论

          本文标题:JavaScript中的继承方式

          本文链接:https://www.haomeiwen.com/subject/mdrmlttx.html