ES5 继承、ES6 class 继承
原型链继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function () { return this.property; }; function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subproperty; }; let instance = new SubType(); console.log(instance.getSuperValue());
|
缺点:
- 实例无法向父类构造函数传参
- 继承单一
- 所有新实例都会共享父类实例的属性(原型上的属性共享,一个是你修改了原型属性,另一个实例的原型属性也会被修改
借用构造函数继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Person(name) { this.name = name; this.sum = function () { alert(this.name); }; } function Con() { Person.call(this, "name"); this.age = 12; } var con1 = new Con(); console.log(con1.name); console.log(con1.age); console.log(con1 instanceof Person);
|
特点:
- 可以传参
- 可以继承多个构造函数属性
缺点:
- 只能继承父类构造函数的属性
- 无法实现构造函数的复用
- 每个新实例都有父类构造函数的副本
组合继承
结合了原型链和盗用构造函数的方法,将两者的优点集中了起来
基本思路是使用原型链继承原型上的属性和方法,使用盗用构造函数继承实例属性
这样既可以把方法定义在原型上以实现重用,又可以让每个实例有自己的属性
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
| function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function () { console.log(this.name); };
function SubType(name, age) { SuperType.call(this, name); this.age = age; }
SubType.prototype = new SuperTuper("Nich", 29); SubType.prototype.sayAge = function () { console.log(this.age); };
let instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); instance1.sayName(); instance1.sayAge();
let instance2 = new SubType("Greg", 27); console.log(instance2.colors); instance2.sayName(); instance2.sayAge();
|
组合继承弥补了原型链和盗用构造函数的不足,是 JS 中使用最多的集成模式
组合继承也保留了 instanceof 操作符和 isPrototypeOf()的识别合成对象的能力
组合继承的效率问题:父类构造函数始终会被调用两次,一次是在创建子类原型时调用,另一次时在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就可以了
原型式继承与 Object.create()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function object(o) { function F() {} F.prototype = o; return new F(); } let person = { name: "Nich", friends: ["Van", "Court"], }; let anotherPerson = object(person);
console.log(anotherPerson.name); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);
|
Object.create()的第二个参数与 Object.defineProperties 第二个参数一样: 每个新增属性都通过各自的描述符来描述,以这种方式添加的属性会遮蔽原型上的同名属性
1 2 3 4 5 6 7 8 9 10
| let person = { name: "Nich", friends: ["Van", "Court"], }; let anotherPerson = Object.create(person, { name: { value: "Greg", }, }); console.log(anotherPerson.name);
|
原型式继承非常适合不需要单独创建构造函数,担任需要在对象间共享信息的场合,但是属性中包含的引用值始终会在相关对象间共享,和使用原型模式式一样的
寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 此函数接收一个参数,就是新对象的基准对象 function createAnother(original) { let clone = object(original) clone.sayHi = function() { console.log('hi') } return clone } let person = { name: 'Nick', friends: ['Bob', 'Van'] } let anotherPerson = createAnother(person) anotherPerson.sayHi()
|
通过寄生式继承给对象添加函数会导致函数难以重用, 与构造函数模式类似
寄生式组合继承
1 2 3 4 5 6 7 8 9 10 11 12 13
| function SubType(name, age) { SuperType.call(this, name); this.age = age; } function inheritPrototype(subType, superType) { let prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function () { console.log(this.age); };
|
寄生式组合继承避免了多次调用 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此效率更高。而且原型键保持不变,instanceof 和 isPrototypeOf()依然有效
寄生式组合继承可以算是引用类型继承的最佳模式
ES6 class 继承
基本用法:class 之间通过使用 extends 关键字完成继承,这比通过修改原型链实现继承方便得多
1 2 3 4 5 6 7 8 9 10 11
| class Vehicle {} class Bus extends Vehicle {} let b = new Bus(); console.log(b instanceof Bus); console.log(b instanceof Vehicle);
function Person() {} class Engineer extends Person {} let e = new Engineer(); console.log(e instanceof Engineer); console.log(e instanceof Person);
|
在类构造函数中使用 super 可以调用父类的构造函数
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 { constructor() { this.hasEngune = true } } class Bus extends Vehicle { constructor() { 不要在super之前引用this,否则会抛出ReferenceError super() console.log(this instanceof Vehicle) console.log(this) } }
class Vehicle { static identify() { console.log('vehicle') } } class Bus extends Vehicle{ static identify() { super.identify() } } Bus.identify()
|
使用 super 的时候需要注意的问题
- 不能 super 只能在派生类构造函数和静态方法中使用
- 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法
- super 会调用父类构造函数,并将返回的实例赋值给 this
- super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
1 2 3 4 5 6 7 8 9 10 11
| class Vehicle { constructor(licensePlate) { this.licensePlate = licensePlate; } } class Bus extends Vehicle { constructor(lP) { super(lP); } } console.log(new Bus("XF18"));
|
- 如果没有定义类构造函数,在实例化派生类的时候会调用 super,并且会传入所有传给派生类的参数
1 2 3 4 5 6 7 8
| class Vehicle { constructor(lp) { this.lp = lp; } } class Bus extends Vehicle {}
console.log(new Bus("XF18"));
|
- 在类构造函数中,不能在 super()之前引用 this
- 如果在派生类中显式地定义了构造函数,则要么必须在其中调用 super,要么必须在其中返回一个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Vehicle {} class Car extends Vehicle {} class Bus extends Vehicle { constructor() { super(); } } class Van extends Vehicle { constructor() { return {}; } } console.log(new Car()); console.log(new Bus()); console.log(new Van());
|
抽象基类
有时候可能需要这样的一种类: 它可以供其它类继承,但是当时本事不能被实例化
虽然专门支持这种类的语法,但是可以通过 new.target 关键字进行判断实现
new.target 保存通过 new 关键字调用的类,在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
| 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();
|
继承内置类型
ES6 类为继承内置应用类型提供了顺畅的机制,可以方便的扩展内置类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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);
|
有些内置类型的方法会返回新的实例,默认情况下返回的实例类型与原始实例的类型是一致的
1 2 3 4 5 6 7
| 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);
|
如果要覆盖这个默认行为,可以覆盖 Symbol.species 访问器
1 2 3 4 5 6 7 8 9 10 11
| 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);
|
类混入
把不同类的行为集中到一个类,ES6 虽然没有显式的对类混入支持,但是现有的特性可以模仿这种行为
1 2 3
| Object.assign()是为了混入对象行为而设计的 只有在需要混入类的行为时才有必要自己实现混入表达式 如果混入的只是对象的属性,那么使用Object.assign()即可
|
如果 Person 类需要组合 A B C,则需要某种机制实现 B 继承 A,C 继承 B,然后 Person 继承 C
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
| 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
| 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();
|