美文网首页
继承或者委托

继承或者委托

作者: 樱木夜访流川枫 | 来源:发表于2018-07-29 19:16 被阅读0次

    一 概览

      这篇文章是我通过阅读《JavaScript 高级程序设计》和《你不知道的JavaScript》中关于 继承 模块的一点心得。

    二 面向对象回顾

    面向类编程

      你是否还记得大学里面刚学C++的时候关于面向对象的介绍呢,让我们一块来回顾一下吧。
      类的 定义:在面向对象编程中,类是一种 代码组织结构形式,一种从真实世界到软件设计的建模方法。
      类的 组织形式:面向对象或者面向类编程强调 数据操作数据的行为 应该 封装 在一起,在正式计算机科学中我们称为 数据结构

    类与23种高级设计模式

      类是面向对象的 底层设计模式,它是面向对象23种高级设计模式的 底层机制
      你或许还听说过 过程化编程,一种不借助高级抽象,仅仅由 过程(函数)调用 来组织代码的编程方式。程序语言中,Java只支持面向类编程,C/C++/Php既支持过程化编程,也支持面向类编程。

    类的机制

      在类的设计模式中,它为我们提供了 实例化继承多态 3种机制。
      构造器:类的实例由类的一种特殊方法构建,这个方法的名称通常与类名相同,称为 “构造器(constructor)”。这个方法的明确的工作,就是初始化实例所需的所有信息(状态)。
      实例化:借助构造函数,由通用类到具体对象的过程。
      继承:子类通过 拷贝(请一定要记住这个词)父类的属性和方法,从而使自己也能拥有这些属性与方法的过程。
      多态:由继承产生的,子类重写从父类中继承的属性和方法,从而子类更加具体。

    类的继承

    (1)相对多态:任何方法都可以引用位于继承层级上更高一层的其他方法(同名或不同名)。我们说“相对”,因为我们不绝对定义我们想访问继承的哪一层(也就是类),而实质上在说“向上一层”来相对地引用。
    (2)超类:在许多语言中,使用 super 关键字来引用 父类或祖先类
    (3)如果子类覆盖父类的某个方法,原版的方法和覆盖后的方法都是可以存在的,允许访问。
    (4) 不要让多态搞糊涂,子类并不是链接到父类上,子类只是父类的一个副本,类继承的实质是拷贝行为
    (5)多重继承:子类的父类不止一个,JavaScript不支持多重继承

    混入

    原理: 子构造函数混入父构造函数的属性和方法。
    JavaScript的复合类型以 引用 的方式传递,不支持拷贝行为。混入(Mixin)手动拷贝 的方式模拟继承的拷贝行为。

    明确混入:

    (1)定义:显示的把一个对象的属性混入另一个对象。
    (2)实现如下:

    // 另一种mixin,对覆盖不太“安全”
    // 大幅简化的`mixin(..)`示例:
    function mixin( sourceObj, targetObj ) {
        for (var key in sourceObj) {
            // 仅拷贝非既存内容
            if (!(key in targetObj)) {
                targetObj[key] = sourceObj[key];
            }
        }
    
        return targetObj;
    }
    
    var Vehicle = {
        engines: 1,
    
        ignition: function() {
            console.log( "Turning on my engine." );
        },
    
        drive: function() {
            this.ignition();
            console.log( "Steering and moving forward!" );
        }
    };
    
    var Car = mixin( Vehicle, {
        wheels: 4,
    
        drive: function() {
            Vehicle.drive.call( this );
            console.log( "Rolling on all " + this.wheels + " wheels!" );
        }
    } );
    

    (3)显示假想多态:Vehicle.drive.call(this)。因为ES6之前,JavaScript无法实现相对多态(inherit:drive()),所以我们明确地用名称指出Vehicle对象,然后在它上面调用drive()函数。
    (4)问题
      A.技术上讲,函数没有被复制,只是复制了函数的引用;
      B.在每一个需要建立 假想多态 引用的函数中都需要建立手动链接(Vehicle.drive.call(this)),维护成本高。可以尝试通过它实现 多重继承
    (5)结论:明确混入复杂、难懂、维护成本高,不推荐使用。

    寄生继承:

    (1)明确的mixin模式的一个变种,在某种意义上是明确的而在某种意义上是隐含的。
    (2)实现如下:在子构造函数中new一个如果找函数的实例对象,在这个对象上扩展属性、方法,最后将这个对象返回。

    // “传统的JS类” `Vehicle`
    function Vehicle() {
        this.engines = 1;
    }
    Vehicle.prototype.ignition = function() {
        console.log( "Turning on my engine." );
    };
    Vehicle.prototype.drive = function() {
        this.ignition();
        console.log( "Steering and moving forward!" );
    };
    
    // “寄生类” `Car`
    function Car() {
        // 首先, `car`是一个`Vehicle`
        var car = new Vehicle();
    
        // 现在, 我们修改`car`使它特化
        car.wheels = 4;
    
        // 保存一个`Vehicle::drive()`的引用
        var vehDrive = car.drive;
    
        // 覆盖 `Vehicle::drive()`
        car.drive = function() {
            vehDrive.call( this );
            console.log( "Rolling on all " + this.wheels + " wheels!" );
        };
        return car;
    }
    
    var myCar = new Car();
    
    myCar.drive();
    // Turning on my engine.
    // Steering and moving forward!
    // Rolling on all 4 wheels!
    

    (3)问题:子函数的初始化创建对象丢失,改变了this绑定,不过不用new去直接创建。

    隐式混入

    (1)定义:父、子构造函数在原有构造函数与属性、方法之间,添加一层函数,子构造函数中间函数的this绑定到父构造函数中间函数
    (2)实现原理:利用了this的二次绑定。
    (3) 实现如下:

    var Something = {
        cool: function() {
            this.greeting = "Hello World";
            this.count = this.count ? this.count + 1 : 1;
        }
    };
    
    Something.cool();
    Something.greeting; // "Hello World"
    Something.count; // 1
    
    var Another = {
        cool: function() {
            // 隐式地将`Something`混入`Another`
            Something.cool.call( this );
        }
    };
    
    Another.cool();
    Another.greeting; // "Hello World"
    Another.count; // 1 (不会和`Something`共享状态)
    

    (4) 问题:单纯的利用this的二次绑定,不能实现相对应用。
    (5) 结论:谨慎使用。

    三 原型

    prototype

    prototype 定义:JavaScript中每个对象都拥有一个prototype属性,它只是一个 其他对象的引用。几乎所有的对象在被创建时,它的这个属性都被赋予了一个 非null 值。

    “类”函数

    代码如下:

    function Foo() {
        // ...
    }
    
    var a = new Foo();
    
    Object.getPrototypeOf( a ) === Foo.prototype; // true
    

    结论: 当通过调用new Foo()创建实例对象时,实例对象会被链接到Foo.prototype指向的对象。

    拷贝与链接

    代码如下:

    function Foo() {
    
    }
    
    Foo.prototype.fruit = ['apple'];
    
    // foo1的[prototype]链接到了 Foo.prototype
    var foo1 = new Foo();
    foo1.fruit.push('banana');
    
    // foo2的[prototype]也被链接到了 Foo.prototype
    var foo2 = new Foo();
    foo2.fruit // [apple, banana]
    

      在面向类的语言中,可以创造一个类的多个拷贝。在JavaScript中,我们不能创造一个类的多个实例,可以创建多个对象,它们的[prototype]链接指向一个共同对象。但默认地,没有拷贝发生,如此这些对象彼此间最终不会完全分离和切断关系,而是 链接在一起。

      “继承”意味着 拷贝 操作,而JavaScript不拷贝对象属性(原生上,默认地)。相反,JS在两个对象间建立链接,一个对象实质上可以将对属性/函数的访问 委托 到另一个对象上。对于描述JavaScript对象链接机制来说,“委托”是一个准确得多的术语。

    new调用和普通调用本质相同

      JavaScript中,new在某种意义上劫持了普通函数,并将它以另一种函数调用:构建一个对象,外加调用这个函数所做的任何事。

    实例对象没有constructor属性
    function Foo() { }
    
    var foo1 = new Foo();
    
    foo1.constructor === Foo  // true
    
    // 修改Foo.prototype指向的对象
    Foo.prototype = {
        //
    }
    
    var foo2 = new Foo();
    
    foo2.constructor === Foo  // false
    

      a.constructor === Foo为true意味着a上实际拥有一个.constructor属性,指向Foo?不对
      实际上,.constructor引用也 委托 到了Foo.prototype,它 恰好 有一个指向Foo的默认属性。

    3 “原型继承”

    原型继承分析

    代码如下:

    
    function Foo(name) {
        this.name = name;
    }
    
    Foo.prototype.myName = function() {
        return this.name;
    };
    
    function Bar(name,label) {
        // 构造函数内部相对多态
        Foo.call( this, name );
        this.label = label;
    }
    
    // 这里,我们创建一个新的`Bar.prototype`链接链到`Foo.prototype`
    Bar.prototype = Object.create( Foo.prototype );
    
    // 注意!现在`Bar.prototype.constructor`不存在了,
    // 如果你有依赖这个属性的习惯的话,可以被手动“修复”。
    Bar.prototype.myLabel = function() {
        return this.label;
    };
    
    var a = new Bar( "a", "obj a" );
    
    a.myName(); // "a"
    a.myLabel(); // "obj a"
    
    

    核心代码分析
    代码1:

    
    function Bar(para1, para2) {
       Foo.call(this, para1);
       //...
    }
    

    代码1分析:构造函数内部初始化,利用this绑定,根据父构造函数初始化子构造函数内部。

    代码2:

    Bar.prototype = Object.create(Foo.prototype)
    

    代码2分析:原型初始化,将子构造函数的[prototype]链接到父构造函数的[prototype]链接的对象。

    误区

    Bar.prototype = Foo.prototype
    

    这种方法是错误的,子构造函数会污染到父构造函数

    ES6 新方法

    Object.setPrototypeOf(Bar.prototype, Foo.prototype)
    
    “自身”

      面向类语言中,根据实例对象查找创建它的类模板,称为自省(或反射)。JavaScript中,如何根据实例对象,查找它的委托链接呢?

    1 instanceOf:

    代码如下:

    function Foo() {
        //...
    }
    
    var a = new Foo();
    
    a instanceOf Foo  // true
    

    代码分析:
    a: instanceOf 机制,在实例对象(a)的原型链中,是否有Foo.prototype;
    b: 需要用于可检测的构造函数(Foo);
    c: 无法判断实例对象间(比如a,b)是否通过[prototype]链相互关联。

    2 isPrototypeOf [[prototype]]反射

    代码如下:

    function Foo() {
        // ...
    }
    
    var a = new Foo();
    
    Foo.prototype.isPrototypeOf(a);  // true
    
    // 对象b是否在a的[[prototype]]链出现过
    b.isPrototypeOf(a);
    

    代码分析
    a:在实例对象(a)的原型链中,是否有Foo.prototype;
    b:需要用于可检测的构造函数(Foo);
    c:可以判断对象间是否通过[prototype]链相互关联。

    3 getPrototypeOf 获取原型链

    代码如下:

    function Foo() {
        // ...
    }
    
    var a = new Foo();
    
    Object.getPrototypeOf(a)  // 查看constructor属性
    

    4 proto

    代码如下:

    function Foo() {
        // ...
    }
    
    var a = new Foo();
    
    a.__proto__ === Foo.prototype  // true
    
    

    代码分析:
    a:proto属性在ES6被标准化;
    b:proto属性跟 constructor属性类似,它不存在实例对象中。constructor属性存在于 原型链中,proto存在于Object.prototype中。
    c:proto看起来像一个属性,但实际上将它看做是一个getter/setter更合适。

    Object.defineProperty( Object.prototype, "__proto__", {
        get: function() {
            return Object.getPrototypeOf( this );
        },
        set: function(o) {
            // setPrototypeOf(..) as of ES6
            Object.setPrototypeOf( this, o );
            return o;
        }
    } );
    

    对象关联

    创建关联

    代码如下:

    var foo = {
        printFoo: function() {
            console.log('foo');
        }
    }
    
    var a = Object.create(foo);
    
    a.printFoo();  // 'foo'
    

    代码分析:
    a、Object.create()会创建一个对象(a),并把它链接到指定对象(foo);
    b、相比new 调用,Object.create()不会产生 prototype引用constructor引用

    关联是否备用

    代码如下:

    var anotherObject = {
        cool: function() {
            console.log('cool');
        }
    }
    
    var bar = Object.create(anotherObject);
    
    bar.cool();  // 'cool'
    

    代码分析:
    a、单纯的在bar无法处理属性或方法时,建立备用链接(anotherObject),代码会变得很难理解和维护,这种模式应该慎重使用;
    b、ES6提供“代理”功能,它实现的就是“方法无法找到”时的行为。

    相关文章

      网友评论

          本文标题:继承或者委托

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