美文网首页
JavaScript 混合对象“类”

JavaScript 混合对象“类”

作者: 游学者灬墨槿 | 来源:发表于2019-01-24 22:16 被阅读3次

    混合对象“类”

    混入

    在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说,JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来。

    【混入】:由于在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也通过模拟类的复制行为创造出一个新的方法,名为“混入”。

    显式混入

    JavaScript 不会自动实现 Vehicle 到 Car 的复制行为,所以我们需要手动实现复制功能。

    【示例】:

    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!");
        }
    });
    

    【注意】:我们处理的已经不再是类了,因为在 JavaScript 中不存在类,Vehicle 和 Car 都是对象,供我们分别进行复制和粘贴。从技术角度来说,函数(ignition)实际上没有被复制,复制的是函数引用。

    1. 再说多态

    上例中 Vehicle.drive.call(this) 这就是我们所说的显式多态。在 ES6 之前的 JavaScript 没有相对多态的机制。所以,由于 Car 和 Vehicle 中都有 drive() 函数,为了指明调用对象,必须使用绝对(而不是相对)引用,即通过名称显式指定 Vehicle 对象并调用它的 drive() 函数。

    【区别】:

    1. 在支持相对多态的面向类的语言中,Car 和 Vehicle 之间的联系只在类定义的开头被创建,从而只需要在这一个地方维护两个类的联系。
    2. 在 JavaScript 中由于屏蔽,使用显式伪多态会在所有需要使用(伪)多态引用的地方创建一个函数关联(我的理解是函数在 JavaScript 中是一等公民,而类在面向类的语言中是一等公民),这会极大地增加维护成本。由于显式伪多态可以模拟多重继承,所以它会进一步增加代码的复杂度和维护难度。

    【建议】:使用伪多态通常会导致代码变得更加复杂,难以阅读并且难以维护,因此应当尽量避免使用显式伪多态,因为这样做往往得不偿失。

    【示例】:显式伪多态模拟多重继承。

    var Vehicle = {
        engine: 1,
        name: function() {
            console.log("I am vehicle!");
        }
    };
    var Car = {
        wheels: 4,
        name: function() {
            console.log("I am car!");
        }
    };
    var BaoMa = {
        name: function() {
            Vehicle.name.call(this);
            Car.name.call(this);
            console.log("I am BaoMa!");
        }
    };
    BaoMa.name();
    // I am vehicle!
    // I am car!
    // I am BaoMa!
    
    2. 混合复制

    【mixin() 工作原理】:遍历 sourceObj(父类)的属性,如果在 targetObj(子类)中没有这个属性就进行复制。由于实在目标对象(子类)初始化之后才进行复制,因此要小心不要覆盖目标对象的原有属性。

    【注意】:如果没有存在性检查,可能会有重写风险。

    【另外一种方法】:先进行复制后对子类进行特殊化,这么做的好处在于不需要进行存在性检查。

    function mixin(sourceObj, targetObj) {
        for(var key in sourceObj) {
            targetObj[key] = sourceObj[key];
        }
        
        return targetObj;
    }
    
    var Vehicle = {
        // ...
    };
    
    // 首先创建一个空对象并把 Vehicle 的内容复制进去
    var Car = mixin(Vehicle, {});
    
    // 然后把新内容复制到 Car 中
    mixin({
        wheels: 4,
        drive: function() {
            // ...
        }
    }, Car);
    

    【问题】:由于两个对象引用的是同一个函数,因此这种复制(或者说混入)实际上并不能完全模拟面向类的语言中的复制。也就是说 JavaScript 中的函数无法真正地复制,所以你只能复制对共享函数对象的引用。如果你修改了共享的函数对象,其所有引用该函数的对象都会受到影响。

    【说明】:显式混入是 JavaScript 中一个很棒的机制,不过它的功能也没有看起来那么强大。虽然它可以把一个对象的属性复制到另一个对象中,但是这其实并不能带来太多的好处,无非就是少几条定义语句,而且还会带来我们刚才提到的函数对象引用问题。

    【注意】:在能够提高代码可读性的前提下使用显式混入,避免使用增加代码理解难度或者让对象关系更加复杂的模式。

    3. 寄生继承

    显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的,主要推广者是 Douglas Crockford。

    【工作原理】:

    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!
    

    【解释】:首先复制一份 Vehicle 父类(对象)的定义,然后混入子类(对象)的定义(如果需要的话保留到父类的特殊引用),然后用这个复合对象构建实例。

    【注意】:调用 new Car() 时会创建一个新对象并绑定到 Car 的 this 上。但是因为我们没有使用这个对象而是返回了我们自己的 car 对象,所以最初被创建的这个对象会被丢弃,因此可以不使用 new 关键字直接调用 Car()。这样做得到的结果是一样的,但是可以避免创建并丢弃多余的对象。

    隐式混入

    隐式混入和之前提到的显式伪多态很像,因此也具备同样的问题。

    【示例】:

    var Something = {
        cool: function() {
            this.greeting = "Hello World";
            this.count = this.count ? this.count + 1 : 1;
        }
    };
    
    var Another = {
        cool: function() {
            // 隐式把 Something 混入 Another
            Something.cool.call(this);
        }
    };
    

    【解释】:通过在构造函数调用或者方法调用中使用 Something.cool.call(this),实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它。最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是 Something 对象上。

    【注意】:虽然这类技术利用了 this 的重新绑定功能,但是 Something.cool.call(this) 仍然无法变成相对引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。

    小结

    • JavaScript 不会像面向类语言那样自动创建对象的副本。
    • 混入模式(无论是显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态,这会让代码更加难懂并且难以维护。此外,显式混入实际上无法完全模拟类的复制行为,因为对象只能复制引用,无法复制被引用对象或者函数本身。忽视这一点会导致许多问题。
    • 总的来说,在 JavaScript 中模拟类是得不尝试的,虽然能解决当前的问题,但是可能会埋下更多的隐患。

    相关文章

      网友评论

          本文标题:JavaScript 混合对象“类”

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