程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • ES6

    • ECMAScript 6 简介
    • let 和 const 命令
    • 变量的解构赋值
    • 字符串的扩展
    • 字符串的新增方法
    • 正则的扩展
    • 数值的扩展
    • 函数的扩展
    • 数组的扩展
    • 对象的扩展
    • 对象的新增方法
    • Symbol
    • Set 和 Map 数据结构
    • Proxy
    • Reflect
    • Promise 对象
    • Iterator 和 for-of 循环
    • Generator 函数的语法
    • Generator 函数的异步应用
    • async 函数
    • Class 的基本语法
    • Class 的继承
      • 1. 简介
      • 2. Object.getPrototypeOf()
      • 3. super 关键字
        • 第一种情况:super 作为函数使用
        • 第二种情况:super 作为对象使用
        • super 的使用注意事项
      • 4. 类的 prototype 属性和 __proto__ 属性
        • Object.setPrototypeOf 方法
        • 两种情况
        • 实例的 __proto__ 属性
      • 5. 原生构造函数的继承
        • 无法继承原生构造函数的示例
        • ES5 继承原生构造函数的问题
        • ES6 继承原生构造函数
        • 自定义原生数据结构子类
        • 自定义 Error 子类
        • 继承 Object 子类的行为差异
      • 6. Mixin 模式的实现
        • 简单的对象合成
        • 完备的 Mixin 实现
        • 使用 Mixin
    • Module 的语法
    • Module 的加载实现
    • 编程风格
    • 读懂 ECMAScript 规格
    • 异步遍历器
    • ArrayBuffer
    • 最新提案
    • 装饰器
    • 函数式编程
    • Mixin
    • SIMD
    • 参考链接
  • ES6
  • ES6
scholar
2024-07-26
目录

Class 的继承

# Class 的继承

# 1. 简介

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

// 定义一个父类 Point
class Point {
}

// 定义一个子类 ColorPoint,继承自 Point
class ColorPoint extends Point {
}
1
2
3
4
5
6
7

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的 constructor(x, y)
    this.color = color; // 定义子类自己的属性
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的 toString()
  }
}
1
2
3
4
5
6
7
8
9
10

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

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

class Point { 
  /* 省略实现细节 */ 
}

class ColorPoint extends Point {
  constructor() {
    // 这里没有调用 super() 方法
  }
}

let cp = new ColorPoint(); // ReferenceError
1
2
3
4
5
6
7
8
9
10
11

上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args); // 自动调用父类的 constructor
  }
}
1
2
3
4
5
6
7
8
9

另一个需要注意的地方是,**在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。**这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError: this is not defined
    super(x, y); // 调用父类的 constructor
    this.color = color; // 正确
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

下面是生成子类实例的代码。

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true,实例 cp 是 ColorPoint 类的实例
cp instanceof Point // true,实例 cp 同时也是 Point 类的实例
1
2
3
4

上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与 ES5 的行为完全一致。

最后,父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // 'hello world',子类 B 继承了父类 A 的静态方法
1
2
3
4
5
6
7
8
9
10

上面代码中,hello()是A类的静态方法,B继承A,也继承了A的静态方法。

# 2. Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true,ColorPoint 的原型对象是 Point
1
2

因此,可以使用这个方法判断,一个类是否继承了另一个类。

class A {}
class B extends A {}

Object.getPrototypeOf(B) === A // true,B 继承了 A
1
2
3
4

上面代码中,通过Object.getPrototypeOf方法,可以判断类B是否继承了类A。

# 3. super 关键字

super 关键字既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

# 第一种情况:super 作为函数使用

当 super 作为函数调用时,它代表父类的构造函数。在子类的构造函数中,必须调用 super 函数,否则会报错。

class A {
  constructor() {
    console.log('A的构造函数');
  }
}

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super(); 
    console.log('B的构造函数');
  }
}

let b = new B();
// 输出:
// A的构造函数
// B的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面代码中,子类 B 的构造函数之中的 super(),代表调用父类 A 的构造函数。如果不调用 super() 方法,子类 B 的构造函数会报错,因为 JavaScript 引擎要求子类的构造函数必须先调用父类的构造函数。

注意,虽然 super 代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B 的实例,因此 super() 在这里相当于 A.prototype.constructor.call(this)。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    // 调用父类的构造函数
    super();
  }
}

new A() // 输出:A
new B() // 输出:B
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,new.target 指向当前正在执行的函数。可以看到,在 super() 执行时,它指向的是子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super() 内部的 this 指向的是 B。

作为函数时,super() 只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

class B extends A {
  m() {
    // 这里调用 super() 会报错
    super(); 
  }
}
1
2
3
4
5
6
7
8

上面代码中,super() 用在 B 类的 m 方法之中,就会造成语法错误。

# 第二种情况:super 作为对象使用

当 super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类本身。

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

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super(); 
    console.log(super.p()); // 2
  }
}

let b = new B();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面代码中,子类 B 当中的 super.p(),就是将 super 当作一个对象使用。这时,super 在普通方法之中,指向 A.prototype,所以 super.p() 就相当于 A.prototype.p()。

这里需要注意,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。

class A {
  constructor() {
    // 定义在实例上的属性
    this.p = 2; 
  }
}

class B extends A {
  get m() {
    // 无法通过 super 调用父类实例上的属性
    return super.p; 
  }
}

let b = new B();
console.log(b.m); // undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面代码中,p 是父类 A 实例的属性,super.p 引用不到它。

如果属性定义在父类的原型对象上,super 就可以取到。

class A {}
// 定义在原型对象上的属性
A.prototype.x = 2; 

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super(); 
    console.log(super.x) // 2
  }
}

let b = new B();
1
2
3
4
5
6
7
8
9
10
11
12
13

上面代码中,属性 x 是定义在 A.prototype 上面的,所以 super.x 可以取到它的值。

# super 的使用注意事项

  1. 在子类的普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例。
class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super();
    this.x = 2;
  }
  m() {
    // 调用父类的 print 方法
    super.print(); 
  }
}

let b = new B();
b.m(); // 输出:2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

上面代码中,super.print() 虽然调用的是 A.prototype.print(),但是 A.prototype.print() 内部的 this 指向子类 B 的实例,导致输出的是 2,而不是 1。也就是说,实际上执行的是 super.print.call(this)。

  1. 在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。
class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    // 调用父类的静态方法
    super.myMethod(msg); 
  }

  myMethod(msg) {
    // 调用父类的实例方法
    super.myMethod(msg); 
  }
}

Child.myMethod(1); // 输出:static 1

let child = new Child();
child.myMethod(2); // 输出:instance 2
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

上面代码中,super 在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

  1. 在子类的构造函数中,必须调用 super 方法,否则会报错。
class A {}

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super(); 
  }
}

let b = new B(); // 不报错
1
2
3
4
5
6
7
8
9
10

上面代码中,子类 B 的构造函数之中的 super(),代表调用父类 A 的构造函数。如果不调用 super() 方法,子类 B 的构造函数会报错,因为 JavaScript 引擎要求子类的构造函数必须先调用父类的构造函数。

  1. 当 super 作为对象时,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super(); 
    console.log(super); // 报错
  }
}
1
2
3
4
5
6
7
8
9

上面代码中,console.log(super) 当中的 super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明 super 的数据类型,就不会报错。

class A {}

class B extends A {
  constructor() {
    // 调用父类的构造函数
    super();
    // 表明 super 是一个对象
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();
1
2
3
4
5
6
7
8
9
10
11
12

上面代码中,super.valueOf() 表明 super 是一个对象,因此就不会报错。同时,由于 super 使得 this 指向 B 的实例,所以 super.valueOf() 返回的是一个 B 的实例。

最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。

let obj = {
  toString() {
    // 调用对象原型链上的 toString 方法
    return "MyObject: " + super.toString();
  }
};

console.log(obj.toString()); // 输出:MyObject: [object Object]
1
2
3
4
5
6
7
8

上面代码中,super.toString() 表示调用对象原型链上的 toString 方法。

# 4. 类的 prototype 属性和 __proto__ 属性

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

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

class B extends A {}  // 定义子类 B,继承父类 A

console.log(B.__proto__ === A);  // true,子类 B 的 __proto__ 属性指向父类 A
console.log(B.prototype.__proto__ === A.prototype);  // true,子类 B 的 prototype 属性的 __proto__ 属性指向父类 A 的 prototype 属性
1
2
3
4
5
6

上面代码中,子类 B 的 __proto__ 属性指向父类 A,子类 B 的 prototype 属性的 __proto__ 属性指向父类 A 的 prototype 属性。

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {}  // 定义父类 A

class B {}  // 定义子类 B

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();
1
2
3
4
5
6
7
8
9
10
11

# Object.setPrototypeOf 方法

《对象的扩展》一章给出过 Object.setPrototypeOf 方法的实现。

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;  // 将 obj 的 __proto__ 属性指向 proto
  return obj;  // 返回修改后的 obj
};
1
2
3
4

因此,就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
1
2
3
4
5
6
7

这两条继承链可以这样理解:作为一个对象,子类(B)的原型(__proto__ 属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype 属性)是父类的原型对象(prototype 属性)的实例。

B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
1
2
3

extends 关键字后面可以跟多种类型的值。

class B extends A {}  // 子类 B 继承父类 A
1

上面代码的 A 只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。

# 两种情况

第一种,子类继承 Object 类

class A extends Object {}  // 子类 A 继承 Object 类

console.log(A.__proto__ === Object);  // true,A 的 __proto__ 属性指向 Object
console.log(A.prototype.__proto__ === Object.prototype);  // true,A 的 prototype 属性的 __proto__ 属性指向 Object.prototype
1
2
3
4

这种情况下,A 其实就是构造函数 Object 的复制,A 的实例就是 Object 的实例。

第二种情况,不存在任何继承

class A {}  // 定义类 A

console.log(A.__proto__ === Function.prototype);  // true,A 的 __proto__ 属性指向 Function.prototype
console.log(A.prototype.__proto__ === Object.prototype);  // true,A 的 prototype 属性的 __proto__ 属性指向 Object.prototype
1
2
3
4

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

# 实例的 __proto__ 属性

子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性。也就是说,子类的原型的原型,是父类的原型。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  printName() {
    console.log(`${this.x}, ${this.y}`);  // 打印实例的 x 和 y 属性
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);  // 调用父类 Point 的构造函数
    this.color = color;  // 添加 color 属性
  }

  printColor() {
    console.log(this.color);  // 打印实例的 color 属性
  }
}

const p1 = new Point(2, 3);  // 创建 Point 类的实例 p1
const p2 = new ColorPoint(2, 3, 'red');  // 创建 ColorPoint 类的实例 p2

console.log(p2.__proto__ === p1.__proto__);  // false,p2 的 __proto__ 属性不等于 p1 的 __proto__ 属性
console.log(p2.__proto__.__proto__ === p1.__proto__);  // true,p2 的 __proto__ 属性的 __proto__ 属性等于 p1 的 __proto__ 属性
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

上面代码中,ColorPoint 继承了 Point,导致前者原型的原型是后者的原型。

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

p2.__proto__.__proto__.printName = function () {
  console.log('Ha');  // 修改父类实例的 printName 方法
};

p1.printName();  // "Ha",调用修改后的 printName 方法
1
2
3
4
5

上面代码在 ColorPoint 的实例 p2 上向 Point 类添加方法,结果影响到了 Point 的实例 p1。

总结

类的 prototype 属性和 __proto__ 属性在继承链中的关系是非常重要的,这样可以帮助我们理解类和实例之间的继承关系以及如何通过原型链来实现方法的继承和属性的共享。

# 5. 原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些:

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

以前,这些原生构造函数是无法继承的,比如,不能自己定义一个 Array 的子类。

# 无法继承原生构造函数的示例

function MyArray() {
  Array.apply(this, arguments); // 尝试继承 Array 的属性和方法
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});
1
2
3
4
5
6
7
8
9
10
11
12

上面代码定义了一个继承 Array 的 MyArray 类。然而,这个类的行为与 Array 完全不一致。

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 输出 0,因为 MyArray 没有正确继承 Array 的 length 属性

colors.length = 0;
console.log(colors[0]); // 输出 "red",因为 MyArray 没有正确继承 Array 的行为
1
2
3
4
5
6

之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。

# ES5 继承原生构造函数的问题

ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上。由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array 构造函数有一个内部属性 [[DefineOwnProperty]],用来定义新属性时更新 length 属性,这个内部属性无法在子类获取,导致子类的 length 属性行为不正常。

下面的例子中,我们想让一个普通对象继承 Error 对象。

var e = {};

console.log(Object.getOwnPropertyNames(Error.call(e))); // [ 'stack' ]

console.log(Object.getOwnPropertyNames(e)); // []
1
2
3
4
5

上面代码中,我们想通过 Error.call(e) 这种写法,让普通对象 e 具有 Error 对象的实例属性。但是,Error.call() 完全忽略传入的第一个参数,而是返回一个新对象,e 本身没有任何变化。这证明了 Error.call(e) 这种写法无法继承原生构造函数。

# ES6 继承原生构造函数

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

class MyArray extends Array {
  constructor(...args) {
    super(...args); // 调用父类 Array 的构造函数
  }
}

var arr = new MyArray();
arr[0] = 12;
console.log(arr.length); // 输出 1,因为正确继承了 Array 的 length 属性

arr.length = 0;
console.log(arr[0]); // 输出 undefined,因为正确继承了 Array 的行为
1
2
3
4
5
6
7
8
9
10
11
12

上面代码定义了一个 MyArray 类,继承了 Array 构造函数,因此就可以从 MyArray 生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如 Array、String 等)的子类,这是 ES5 无法做到的。

# 自定义原生数据结构子类

上面这个例子也说明,extends 关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面是定义了一个带版本功能的数组。

class VersionedArray extends Array {
  constructor() {
    super();
    this.history = [[]]; // 初始化历史记录数组
  }

  commit() {
    this.history.push(this.slice()); // 将当前数组快照存入历史记录
  }

  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]); // 恢复数组到最近的历史记录
  }
}

var x = new VersionedArray();

x.push(1);
x.push(2);
console.log(x); // [1, 2]
console.log(x.history); // [[]]

x.commit();
console.log(x.history); // [[], [1, 2]]

x.push(3);
console.log(x); // [1, 2, 3]
console.log(x.history); // [[], [1, 2]]

x.revert();
console.log(x); // [1, 2]
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

上面代码中,VersionedArray 会通过 commit 方法,将自己的当前状态生成一个版本快照,存入 history 属性。revert 方法用来将数组重置为最新一次保存的版本。除此之外,VersionedArray 依然是一个普通数组,所有原生的数组方法都可以在它上面调用。

# 自定义 Error 子类

下面是一个自定义 Error 子类的例子,可以用来定制报错时的行为。

class ExtendableError extends Error {
  constructor(message) {
    super();
    this.message = message; // 错误信息
    this.stack = (new Error()).stack; // 错误栈信息
    this.name = this.constructor.name; // 错误名称
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m); // 调用父类 ExtendableError 的构造函数
  }
}

var myerror = new MyError('ll');
console.log(myerror.message); // "ll"
console.log(myerror instanceof Error); // true
console.log(myerror.name); // "MyError"
console.log(myerror.stack);
// Error
//     at MyError.ExtendableError
//     ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 继承 Object 子类的行为差异

注意,继承 Object 的子类,有一个 行为差异 (opens new window)。

class NewObj extends Object {
  constructor() {
    super(...arguments); // 尝试将参数传递给 Object 构造函数
  }
}

var o = new NewObj({ attr: true });
console.log(o.attr === true); // false
1
2
3
4
5
6
7
8

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

总结

通过 ES6 的 extends 关键字和 super 关键字,我们可以轻松地继承和扩展原生构造函数,实现更多自定义的数据结构和功能,同时保持原生构造函数的行为和特性。

# 6. Mixin 模式的实现

Mixin 指的是将多个对象的属性和方法合并到一个新的对象中,使新对象具有所有组成对象的接口。下面是 Mixin 模式的一个简单实现。

# 简单的对象合成

const a = {
  a: 'a'
};

const b = {
  b: 'b'
};

const c = { ...a, ...b }; // 使用对象扩展运算符进行合成
console.log(c); // 输出 { a: 'a', b: 'b' }
1
2
3
4
5
6
7
8
9
10

上面代码中,c对象是a对象和b对象的合成,具有两者的接口。这个实现仅限于简单对象的合成。

# 完备的 Mixin 实现

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  // 定义一个新类 Mix
  class Mix {
    constructor() {
      // 遍历所有的 mixin 类,将其实例属性复制到新类的实例上
      for (let mixin of mixins) {
        copyProperties(this, new mixin());
      }
    }
  }

  // 遍历所有的 mixin 类,将其静态属性和原型属性复制到新类上
  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  // 返回新类
  return Mix;
}

function copyProperties(target, source) {
  // 使用 Reflect.ownKeys 获取 source 的所有属性,包括不可枚举属性和 Symbol 属性
  for (let key of Reflect.ownKeys(source)) {
    // 排除 constructor、prototype 和 name 属性
    if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc); // 将属性复制到目标对象
    }
  }
}
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

上面代码定义了一个 mix 函数,用于将多个类的接口合成到一个新类中。使用这个函数时,只需让新类继承合成类即可。

# 使用 Mixin

class Loggable {
  log() {
    console.log('Logging...');
  }
}

class Serializable {
  serialize() {
    return JSON.stringify(this);
  }
}

// 继承 mix 函数生成的合成类
class DistributedEdit extends mix(Loggable, Serializable) {
  // 可以在这里添加 DistributedEdit 类特有的方法和属性
}

const instance = new DistributedEdit();
instance.log(); // 调用 Loggable 的方法,输出 'Logging...'
console.log(instance.serialize()); // 调用 Serializable 的方法,输出实例的 JSON 字符串表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

上面代码中,DistributedEdit 继承了 mix 函数生成的合成类,因此具有 Loggable 和 Serializable 的所有方法。这样,DistributedEdit 类可以同时拥有日志记录和序列化的功能。

总结

Mixin 模式通过将多个类的属性和方法合并到一个新类中,实现了多重继承的效果。使用 Mixin 可以让类的设计更加灵活和模块化,适用于需要将不同功能模块组合在一起的场景。

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Class 的基本语法
Module 的语法

← Class 的基本语法 Module 的语法→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式