你不知道的JS系列上( 45 ) - 显式混入

Posted Zina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你不知道的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 父类的定义,然后混入子类的定义(如果需要的话保留父类的特殊引用),然后用这个复合对象构建实例。

 

以上是关于你不知道的JS系列上( 45 ) - 显式混入的主要内容,如果未能解决你的问题,请参考以下文章

你不知道的JS系列 ( 12 ) - 声明提升

你不知道的js-混合对象-类

你不知道的JS系列- 引擎怎么查找变量

你不知道的JS系列 ( 5 ) - 词法作用域

你不知道的JS系列 ( 7 ) - 欺骗词法作用域

你不知道的JS系列 ( 14 ) - 闭包无处不在