时间是坦诚的,也是公平的,一年如一日地做一件事确实很难,但这才是坚持的意义。
今日学习内容
1、JS 红皮书 P262-265 第八章:对象、类与面向对象编程
今日笔记
1、抽象基类: 有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。虽然 ECMAScript 没有专门支持这种类的语法 ,但通过 new.target 也很容易实现。new.target 保存通过 new 关键字调用的类或函数。通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Vehicle { constructor() { console.log(new.target); if (new.target === Vehicle) { throw new Error("Vehicle cannot be directly instantiated"); } } }
class Bus extends Vehicle {} new Bus(); new Vehicle();
|
另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Vehicle { constructor() { if (new.target === Vehicle) { throw new Error("Vehicle cannot be directly instantiated"); } if (!this.foo) { throw new Error("Inheriting class must define foo()"); } console.log("success!"); } }
class Bus extends Vehicle { foo() {} }
class Van extends Vehicle {} new Bus(); new Van();
|
2、继承内置类型: ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class SuperArray extends Array { shuffle() { for (let i = this.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this[i], this[j]] = [this[j], this[i]]; } } } let a = new SuperArray(1, 2, 3, 4, 5); console.log(a instanceof Array); console.log(a instanceof SuperArray); console.log(a); a.shuffle(); console.log(a);
class SuperArray extends Array {} let a1 = new SuperArray(1, 2, 3, 4, 5); let a2 = a1.filter((x) => !!(x % 2)); console.log(a1); console.log(a2); console.log(a1 instanceof SuperArray); console.log(a2 instanceof SuperArray);
class SuperArray extends Array { static get [Symbol.species]() { return Array; } } let a1 = new SuperArray(1, 2, 3, 4, 5); let a2 = a1.filter((x) => !!(x % 2)); console.log(a1); console.log(a2); console.log(a1 instanceof SuperArray); console.log(a2 instanceof SuperArray);
|
3、类混入: 把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。
注意 Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用 Object.assign()就可以了。
1 2 3 4 5 6 7
| class Vehicle {} function getParentClass() { console.log("evaluated expression"); return Vehicle; } class Bus extends getParentClass() {}
|
一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Vehicle {} let FooMixin = (Superclass) => class extends Superclass { foo() { console.log('foo'); } }; let BarMixin = (Superclass) => class extends Superclass { bar() { console.log('bar'); } }; let BazMixin = (Superclass) => class extends Superclass { baz() { console.log('baz'); } }; class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} let b = new Bus(); b.foo(); b.bar(); b.baz();
|
通过写一个辅助函数,可以把嵌套调用展开:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Vehicle {} let FooMixin = (Superclass) => class extends Superclass { foo() { console.log('foo'); } }; let BarMixin = (Superclass) => class extends Superclass { bar() { console.log('bar'); } }; let BazMixin = (Superclass) => class extends Superclass { baz() { console.log('baz'); } }; function mix(BaseClass, ...Mixins) { return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); } class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} let b = new Bus(); b.foo(); b.bar(); b.baz();
|
注意 很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。
4、小结: 对象在代码执行过程中的任何时候都可以被创建和增强,具有极大的动态性,并不是严格定义的实体。下面的模式适用于创建对象。
工厂模式就是一个简单的函数,这个函数可以创建对象,为它添加属性和方法,然后返回这个对象。这个模式在构造函数模式出现后就很少用了。
使用构造函数模式可以自定义引用类型,可以使用 new 关键字像创建内置类型实例一样创建自定义类型的实例。不过,构造函数模式也有不足,主要是其成员无法重用,包括函数。考虑到函数本身是松散的、弱类型的,没有理由让函数不能在多个对象实例间共享。
原型模式解决了成员共享的问题,只要是添加到构造函数 prototype 上的属性和方法就可以共享。而组合构造函数和原型模式通过构造函数定义实例属性,通过原型定义共享的属性和方法。
JavaScript 的继承主要通过原型链来实现。原型链涉及把构造函数的原型赋值为另一个类型的实例。这样一来,子类就可以访问父类的所有属性和方法,就像基于类的继承那样。原型链的问题是所有继承的属性和方法都会在对象实例间共享,无法做到实例私有。盗用构造函数模式通过在子类构造函数中调用父类构造函数,可以避免这个问题。这样可以让每个实例继承的属性都是私有的,但要求类型只能通过构造函数模式来定义(因为子类不能访问父类原型上的方法)。目前最流行的继承模式是组合继承,即通过原型链继承共享的属性和方法,通过盗用构造函数继承实例属性。
除上述模式之外,还有以下几种继承模式。
原型式继承可以无须明确定义构造函数而实现继承,本质上是对给定对象执行浅复制。这种操作的结果之后还可以再进一步增强。
与原型式继承紧密相关的是寄生式继承,即先基于一个对象创建一个新对象,然后再增强这个新对象,最后返回新对象。这个模式也被用在组合继承中,用于避免重复调用父类构造函数导致的浪费。
寄生组合继承被认为是实现基于类型继承的最有效方式。
ECMAScript 6 新增的类很大程度上是基于既有原型机制的语法糖。类的语法让开发者可以优雅地定义向后兼容的类,既可以继承内置类型,也可以继承自定义类型。类有效地跨越了对象实例、对象原型和对象类之间的鸿沟。