ES6学习笔记23:Class的继承

0 阅读1分钟

@[toc]

简介

Class 可以通过关键字extends实现继承。这比ES5通过修改原型链实现继承要清晰、方便。

class Point {
}

class ColorPoint extends Point{
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

子类必须在constructor方法中调用super方法,否则新建实例的时候会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样实例属性和方法,然后再对其进行加工,加上子类自己的属性和方法。如果不调用super,子类就得不到this对象。

ES5 & ES6 继承实质比较

ES5 的继承,实质是先创造子类的实例对象(this),然后将负累的方法添加到this上面(Parent.apply(this))。

ES6 的继承,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后在用子类的构造函数修改this.

注意

  • 如果子类没有定义constructor方法,这个方法会默认被添加,不管有没有显式定义,任何一个子类都有一个constructor方法。
class ColorPoint extends Point {
}

// 等同于

class ColorPoint extends Point {
  constructor(...args){
    super(...args);
  }
}
  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
class Point {
  constructor(x,y){
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x,y,color){
    this.color = color; // 报错
    super();
    this.color = color; // OK 
  }
}
  • 父类的静态方法,也会被子类继承

Object.getPrototypeOf()

方法Object.getPrototypeOf()可以从子类上获取父类

Object.getPrototypeOf(ColorPoint) === Point; // true

super 关键字

关键字super,既可以当作函数使用,也可以当作对象使用。

  • 当作函数

关键字super作为函数调用时,代表父类的构造函数。子类的构造函数必须执行一次super函数。

class A{}

class B extends A {
  constructor() {
    super();
  }
}

注意super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。并且作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

  • 当作对象

在普通方法中,指向父类的原型对象;在静态方法中,指向父类

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor(){
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

注意:

  1. 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性是无法通过super调用的。
  2. 如果属性定义在父类的原型对象上,super就可以取到,
  3. 在静态方法中,这时super将指向父类,而不是父类的原型对象。
  4. 在子类静态方法中,通过super调用父类方法时,方法内部的this指向当前的子类,而不是子类的实例。
  5. 使用super的时候,必须显式指定是作为函数还是作为对象使用,否则会报错。
  6. 由于对象总是继承其他对象的,所以在任意一个对象中,使用super关键字。

类的prototype 属性和 _proto_属性

大多数浏览器的ES5 实现中,每一个对象都有_proto_属性,指向对应的构造函数的prototype属性。Class同时具有prototype属性和_proto_属性,因此同时存在两条继承链。

  1. 子类的_proto_属性,表示构造函数的继承,总是指向父类
  2. 子类的prototype属性的_proto_属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}

class B extends A {
}

B._proto_ === A; // true
B.prototypr._ptoto_ = A.prototype; // true
  • 子类继承Object
class A extends Object{
}

A._proto_ === Object; // true
A.prototype._proto_ === Object.prototype; // true

在这里A其实就是构造函数Object的复制,A的实例就是Object的实例

  • 不存在任何继承
class A {
  
}

A._proto_ === Function.prototype; // true
A.prototype._proto_ = Object.prototype; // true

在这里,A作为一个基类(不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(Object实例)。所以:A.prototype._proto_指向构造函数(Object)的prototype属性

实例的_proto_属性

子类实例的_proto_属性的_proto_属性,指向父类实例的_proto_属性。子类的原型的原型是父类的原型。

因此,通过子类实例的_proto_._proto_属性,可以修改父类实例行为

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。

ECMAScript 的原生构造函数大致一下这些:

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object

ES6 允许继承原生构造函数定义子类,因为ES6新建的是父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

例子:继承Array

class MyArray extends Array{
  constuctor(...args){
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length; // 1

arr.length = 0;
arr[0]; // undefined

这里关键字extends不仅可以用来继承类,还可以用来继承原生的构造函数。

***注意:***继承Object的子类,有一个行为差异

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var obj = new NewObj({attr: true});

o.attr === true; // false

在这里,NewObj继承了Object,但是无法通过super方法向父类Object传参。这里因为ES6改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种方式调用的,ES6 规定Object构造函数会忽略参数。

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

  • 一个简单的实现
const a = {
  a: '1'
}

const b = {
  b: '2'
}

const c = {...a, ...b}; // {a: 'a', b: 'b'};
  • 更完善的实现
function mix(...mixins) {
	class Mix {
			constructor() {
				for (const mixin of mixins) {
				copyProperties(this, new mixin()); // 拷贝实例属性
			}
		}
	}

	for (const mixin of mixins) {
		copyProperties(this, new mixin()); // 拷贝静态属性
		copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
	}
		return Mix;
}

function copyProperties(target, source) {
	for (const key of Reflect.ownKeys(source)) {
		if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
			const desc = Object.getOwnPropertyDescriptor(source, key);
			Object.defineProperty(target, key, desc);
		}
	}
}

备注:本文是自己学习阮一峰老师的《ECMAScript 6 入门》所做的笔记,大部分例子来源于此书。