• 浅谈 JavaScript 中的继承模式


    最近在读一本设计模式的书,书中的开头部分就讲了一下 JavaScript 中的继承,阅读之后写下了这篇博客作为笔记。毕竟好记性不如烂笔头。

    JavaScript 是一门面向对象的语言,但是 ES6 之前 JavaScript 是没有类这个概念的。即使 ES6 引入了 class,也只是基于 JavaScript 已有的原型继承模式的一个语法糖,并没有为 JavaScript 引入新的面向对象的继承模型。

    但是 JavaScript 是一门非常灵活的语言,为了实现类和继承,JavaScript Developers 已经把原型玩出了花,下面介绍一下 JavaScript 中的继承模式。

    JavaScript 中已有的继承模式包括以下六种:

    • 类式继承
    • 构造函数式继承
    • 组合式继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式继承

    一、类式继承

    JavaScript 中,每个对象都有一个原型(prototype),在对象本身找不到对应属性的时候,JavaScript 就会去对象的原型上找,所以这就为 JavaScript 实现继承提供了一种方法 —— 我们只需要把父类的属性放在子类的原型上就行了。因为子类中没有对应属性的话,就会使用原型上的属性,即父类的属性,这就达到了继承的目的。

    // 父类
    function Person() {
        this.name = 'Kevin';
        this.age = 18;
    }
    // 子类
    function Chinese() {
        this.feature = 'hard-working';
    }
    // 指定子类的原型,实现子类继承父类。
    Chinese.prototype = new Person();
    // 实例化子类。
    var c = new Chinese();
    // 输出'hard-working'。
    console.log(c.feature);
    // 输出'Kevin'。
    console.log(c.name);
    

      

    这是一种比较简单直观的继承实现方法,但是存在着问题。所有的子类实例共享了父类实例的属性。如果父类实例的属性是引用类型的值时,将会出现牵一发而动全身的风险。

    比如,我们给父类 Person 添加一个 category 的数组属性:

    // 父类
    function Person() {
        this.name = 'Kevin';
        this.age = 18;
        this.category = ['yellow', 'black', 'white'];
    }
    // 子类
    function Chinese() {
        this.feature = 'hard-working';
    }
    // 指定子类的原型,实现子类继承父类。
    Chinese.prototype = new Person();
    // 实例化子类。
    var c1 = new Chinese();
    var c2 = new Chinese();
    // 修改 c1 的 category 属性,世界上还有棕色人种。
    c1.category.push('brown');
    // 输出 c1 和 c2 的 category 属性。
    console.log(c1.category);
    // 结果是 ["yellow", "black", "white", "brown"]
    console.log(c2.category);
    // 结果是 ["yellow", "black", "white", "brown"]
    

      

    我们只修改了 c1 的 category 属性,但是 c2.category 也跟着变化了,因为它们共享了一个 Person 实例,而这个 Person 实例的 category 值是引用类型的。

    这就是一个很严重的缺陷了,这样的继承模式会使得我们的数据变得不可预测。

    二、构造函数式继承

    类式继承的致命缺陷是共享引用类型的属性导致牵一发而动全身,而问题的根源在于我们给子类的原型赋值为父类的一个实例,那现在解决这个问题就有办法了。我们可以麻烦一点,不把父类的实例赋值给原型了,直接赋值给每一个子类实例吧,这样就不会存在共享引用类型属性的问题了。

    // 父类
    function Person() {
        this.name = 'Kevin';
        this.age = 18;
        this.category = ['yellow', 'black', 'white'];
    }
    // 子类
    function Chinese() {
        Person.call(this);
        this.feature = 'hard-working';
    }
    // 实例化子类。
    var c1 = new Chinese();
    var c2 = new Chinese();
    // 修改 c1 的 category 属性。
    c1.category.push('brown');
    // 输出 ["yellow", "black", "white", "brown"]
    console.log(c1.category);
    // 输出 ["yellow", "black", "white"]
    console.log(c2.category);
    

      

    可以看到,共享引用类型属性的问题确实解决了。但是这种方法也不太好,因为我们没有利用好原型。每个属性都实例化在实例的本身,造成了资源的浪费,也不符合代码复用的原则。对于一些共享的公用的方法,我们应该绑定在原型上。

    三、组合式继承

    为了解决第二小节的问题,出现了组合式继承。所谓组合式继承,就是综合类式继承和构造函数式继承,结合二者。

    比如:

    // 父类
    function Person() {
        this.name = 'Kevin';
        this.age = 18;
        this.category = ['yellow', 'black', 'white'];
    }
    // 子类
    function Chinese() {
        // 基于构造函数式继承。
        Person.call(this);
        this.feature = 'hard-working';
    }
    // 基于类式继承。
    Chinese.prototype = new Person();
    console.log(new Person());
    console.log(new Chinese());
    

      

    这样就“充分地”利用了原型。

    但是什么东西都往原型塞,也是一种浪费。我们在子类构造函数中调用了父类的构造函数,在指定子类的原型时又调用了一遍,既不优雅又浪费资源,所以这种方法也不怎么样。

    四、原型式继承

    这是 2006 年道格拉斯.克罗克福德的一篇文章所提出来的,我们把要继承的属性放在原型中,然后再在实例上定义属性,这样就使得每个实例可以有自己特有的属性,还可以和其他实例共享必要的属性。

    function inherit(o) {
        function F(){}
        F.prototype = o;
        return new F();
    }
    var x = inherit({name: 'Kevin'});
    x.age = 18;
    // 输出 {name: 'Kevin', age: 18}
    console.log(x);
    

      

    这种方法首先定义了一个空白的构造函数,然后为其指定了将要继承的属性,然后返回它的一个实例,最后给每个实例指定属性。

    这样节约了很多的资源,每个返回的实例也很干净纯洁。

    五、寄生式继承

    这是一种基于原型式继承的方法,只多了一个步骤,就是给每个实例添加属性。这看起来像是第五小节代码的一种封装。

    function inherit(o) {
        function F(){}
        F.prototype = o;
        var f = new F();
        f.name = 'Kevin';
        f.age = 18;
        return f;
    }
    var proto = {
        sayName: function() {
            console.log(this.name);
        }
    };
    var x = inherit(proto);
    // {name: "Kevin", age: 18}
    console.log(x);
    // Kevin
    x.sayName();
    

      

    这种继承方式在 underscore 中曾经使用过,具体哪里我也不记得了,感兴趣的同学可以去看看。

    六、寄生组合式继承

    这是一种综合型的方法,结合了寄生式、组合式继承方法。实际上是寄生式、构造函数式、类式继承的综合体。

    // 基于寄生式继承实现的原型赋值,把子类的原型赋值为父类。
    function inheritProto(SubClass, SuperClass) {
        function F() {}
        F.prototype = SuperClass.prototype;
        var f = new F();
        // 赋值 prototype 会丢失 constructor 指向,重新赋值。
        f.constructor = SubClass;
        SubClass.prototype = f;
    }
    // 父类
    function Person() {
        this.name = 'Kevin';
        this.age = 18;
    }
    Person.prototype.sayName = function() {
        console.log(this.name);
    };
    function Chinese() {
        // 构造函数式继承。
        Person.call(this);
        this.feature = 'hard-working';
        this.name = '李国强';
    }
    // 类式继承原型。
    inheritProto(Chinese, Person);
    var c = new Chinese();
    console.log(c);
    // 输出 {name: "李国强", age: 18, feature: "hard-working"}
    c.sayName();
    // 输出 "李国强"。
    

      

    从注释可以看出来,确实综合了三种继承模式。所以这种方法的名字就叫寄生组合式继承。

    笔记一篇,时间很仓促,如果有问题欢迎指正!

    所有代码可以复制到浏览器控制台执行查看输出。

  • 相关阅读:
    wpf数据验证实例及常用方法小结
    wpf自定义colorpicker
    DataGrid绑定Dictionary问题
    DataTemplate和ControlTemplate的关系
    Validation Rule和Binding Group
    WPF converter(包含传递复杂参数)
    【链接】Eclipse的Debug调试技巧
    安装apache服务出错,无法启动此程序,因为计算机中丢失VCRUNTIME140.dll
    apache解压版安装服务
    apache——(OS 10048)通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 : AH00072: make_sock: could not bind to address [::]:443
  • 原文地址:https://www.cnblogs.com/DM428/p/9539285.html
Copyright © 2020-2023  润新知