# YDKJS-类

类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。

在相当长的一段时间里,JavaScript 只有一些近似类的语法元素(比如 new 和 instanceof),不过在后来的 ES6 中新增了一些元素,比如 class 关键字。

class 关键字的出现并不意味着在 JavaScript 中有类,因为 JavaScript 的机制其实和类完全不同。

事实上,在软件设计中类是一种可选的模式,你可以根据需要自己根据决定是否在 JavaScript 中使用它。

# 构造函数

类和实例的关系就好比建筑蓝图和一栋建筑,图中设计了一栋建筑应该具备的结构,建筑工人会按照蓝图建造建筑,之后建筑工人也可以到下一个地方,把所有工作都重复一遍,再创建一份副本。

具体的创造细节是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。

class CoolGuy {
  specialTrick = nothing

  CoolGuy(trick) {
    specialTrick = trick
  }

  showOff() {
    output("Here's my trick: ", specialTrick)
  }
}

我们可以调用类构造函数来生成一个 CoolGuy 实例:

var Joe = new CoolGuy('jumping rope') // 实际上调用的就是构造函数 CoolGuy(),它会返回一个对象(也就是类的实例)
Joe.showOff() // 这是我的绝技:跳绳

类构造函数属于类,而且通常和类同名。此外,构造函数大多需要用 new 来调,这样语言引擎才知道你想要构造一个新的类实例。

# 继承

在面向类的语言中,你可以先定义一个类,然后定义一个继承前者的类。后者通常被称为“子类”,前者通常被称为“父类”(基类)。

定义好一个子类之后,相对于父类来说它就是一个独立并且完全不同的类。子类会包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新行为。

class Vehicle {
  engines = 1;

  ignition() {
    output("Turning on my engine.");
  }

  drive() {
    ignition();
    output("Steering and moving forward!");
  }
}

class Car inherits Vehicle {
  wheels = 4;

  drive() {
    inherited: drive();
    output("Rolling on all ", wheels, " wheels!");
  }
}

class SpeedBoat inherits Vehicle {
  engines = 2;

  ignition() {
    output("Turning on my ", engines, " engines.");
  }

  pilot() {
    inherited: drive();
    output("Speeding through the water with ease!");
  }
}

我们通过定义 Vehicle 类来假设一种发动机,一种点火方式,一种驾驶方法。但不可能制造一个通用的“交通工具”,因为这个类只是一个抽象的概念。

接下来我们定义了两类具体的交通工具:Car 和 SpeedBoat。它们都从 Vehicle 继承了通用的特性并根据自身类别修改了某些特性。汽车需要四个轮子,快艇需要两个动机,因此它必须启动两个发动机的点火装置。

# 多态

在上面的示例中,Car 重写了继承自父类的 drive() 方法,但是之后 Car 调用了 inherited:drive() 方法,这表明 Car 可以引用继承来的原始 drive() 方法。快艇的 pilot() 方法同样引用了原始 drive() 方法。

这个技术被称为多态或者虚拟多态。在本例中,更恰当的说法是相对多态:任何方法都可以引用继承层次中高层的方法(无论高层的方法名和当前方法名是否相同)。

在许多语言中可以使用 super 来代替本例中的 inherited:,它的含义是“超类”(superclass),表示当前类的父类/祖先类。

多态的另一个方面是,在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义。

在之前的代码中就有两个这样的例子:drive() 被定义在 Vehicle 和 Car 中,ignition() 被定义在 Vehicle 和 SpeedBoat 中。

多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。

# 混入

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

由于在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。接下来我们会看到两种类型的混入:显式和隐式。

# 显式混入

非常简单的 mixin(..) 例子 :

function mixin(sourceObj, targetObj) {
  for (var key in sourceObj) {
    // 只会在不存在的情况下复制
    if (!(key in targetObj)) {
      targetObj[key] = sourceObj[key]
    }
  }

  return targetObj
}

回顾一下之前提到的 Vehicle 和 Car。由于 JavaScript 不会自动实现 Vehicle 到 Car 的复制行为,所以我们需要手动实现复制功能:

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!')
  },
})

现在 Car 中就有了一份 Vehicle 属性和函数的副本了(从技术角度来说,函数实际上没有被复制,复制的是函数引用)。

在支持相对多态的面向类的语言中,Car 和 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!')
}

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 父类(对象)的定义,然后混入子类(对象)的定义(如果需要的话保留到父类的特殊引用),然后用这个复合对象构建实例。

# 隐式混入

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

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 (count 不是共享状态)

通过在构造函数调用或者方法调用中使用 Something.cool.call( this ),我们实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它。

最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是 Something 对象上。因此,我们把 Something 的行为“混入”到了 Another 中。