你不知道的JS系列上( 45 ) - 显式混入
JS 的对象机制并不会自动执行复制行为,由于其他语言中表现出来的复制行为,因此 JS 开发者也想出了一个方式来模拟类的复制行为,这个方法就是混入。我们先看第一种,显式混入。
// 非常简单的 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, { wheel: 4, drive: function(){ Vehicle.drive.call(this); console.log('Rolling on all ' + this.wheel + ' wheels!'); } })
我们处理的已经不再是类了,因为在 JS 中不存在类,Vehicle 和 Car 都是对象。现在 Car 中就有了一份 Vehicle 属性和函数的副本。从技术角度来说,函数实际上没有被复制,复制的是函数引用。Car 已经有了 drive 属性,所以这个属性引用没有被 mixin 重写,从而保留了 Car 中定义的同名属性,实现了 ‘子类’ 对 ‘父类’ 属性的重写。
我们再类分析一下这条语句 Vehicle.drive.call(this)。 这就是所说的显式多态。之前我们代码中 super.drive(),我们称之为相对多态。由于 Car 和 Vehicle 中都有 drive() 函数,为了指明调用对象。我们通过名称显示制定 Vehicle 对象并调用它的 drive() 函数。
但是如果直接执行 Vehicle.drive(),函数调用中的 this 会被绑定到 Vehicle 对象而不是 Car 对象,这并不是我们想要的。
我们分析一下 mixin 的工作原理。它会遍历 sourceObj 的属性,如果在 targetObj 没有这个属性就会进行复制。
如果我们是先进行复制然后对 Car 进行特殊化对话,就可以跳过存在性检查。不过这种方法并不好并且效率更低,所以不如第一种方法常用:
// 另一种混入函数,可能又重写风险 function mixin(sourceObj, targetObj) { for (var key in sourceObj) { targetObj[key] = sourceObj[key]; } return targetObj; }
var Vehicle = { // ... }
// 首先创建一个空对象并把 Vehicle 对内容复制进去 var Car = mixin( Vehicle, {} ); // 然后把新内容复制到 Car 中 mixin({ wheel: 4, drive: function() { // ... } }, Car)
这两种方法都可以把不重叠对内容从 Vehicle 中显示复制到 Car 中。‘混入’ 这个名字来源与这个过程对另一种解释: Car 中混合了 Vehicle 的内容,所以这叫混合复制。复制操作完成后, Car 和 Vehicle 分离了,向 Car 中添加属性不会影响到 Vehicle,反之亦然。
JS 中的函数无法真正的复制,所以只能复制对共享函数的引用,如果修改了共享函数,那么 Vehicle 和 Car 都会受到影响。
显示混入是 JS 中一个很棒的机制,不过它的功能也没有看起来那么强大。虽然它可以把一个对象的属性复制到另一个对象中,但是这其实并不能带来太多的好处,无非是少几条定义语句,而且还会带来我们能提到的函数对象引用问题。
之前我们说只有父类和子类,这个比喻不太恰当,因为现实生活中,除了父类,还有母类。绝大多数后代是由双亲产生的。如果类可以继承两个类,那看起来更符合现实的比喻了。有些面向类的语言允许继承多个‘父类’。多重继承意味着所有父类的定义都会复制到子类中。
JS 中本身并不提供多重继承的功能,许多人认为这是件好事,因为使用多重继承的代价太高,然而这无法阻止开发者门的热情,他们会尝试多种方法来实现多重继承
比如此例中,如果向目标函数显示混入多个对象,就可以部分模仿多重继承行为,但是从根本上来说,使用这些‘诡计’通常会得不偿失
显示混入模式还有一种变体被称为 “寄生继承”
// 传统的 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();
首先我们复制一份 Vehicle 父类的定义,然后混入子类的定义(如果需要的话保留父类的特殊引用),然后用这个复合对象构建实例。