ES6 面向对象编程的语法糖

三葉Leaves Author

书接上文: 继承的手写实现

既然我们已经搞明白继承的原理了,下面我们就可以看看 ES6 里是怎么做的了。

还是那个模态框的例子,这次我们使用 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
class Modal {
constructor(title, content) {
this.title = title
this.content = content
}

show() {
console.log(`title 为 ${this.title} 的模态框显示了`);
}
// 相比之前,我多定义了一个静态方法,之后会用到
static isModal =function (Object) {
return Object instanceof Modal
}
}

class Modal_plus extends Modal {
constructor(title, content, theme) {
// 这条 super 语句一定要写在 this 语句的前面
super(title, content)
this.theme = theme
}
changeTitle() {
this.title = 'JOKE'
}
}

单从代码量上看,就简洁的不止一点了。

关于静态方法的继承

看懂很简单,但是底层的细节还是值得深挖的。先说一下功能上的一个显著不同点:

在之前,对于挂在构造函数上的静态方法,ES5 的方式子类并不会继承父类的静态方法

1
2
3
4
5
// 🌟父类的定义部分加上这一条:定义父类的静态方法

Modal.isModal = function (Object) {
return Object instanceof Modal
}

这在 ES6 里是用 static 关键字写在构造函数里的。关于静态方法、实例方法、对象自带方法的区别,可见 原型对象与方法

定义好以后,我们来到测试部分,让子类试图调用父类的这个静态方法:

1
console.log(Modal_plus.isModal(modal));

会报错:

1
2
Uncaught TypeError: Modal_plus.isModal is not a function
at hand.js:46:24

这说明了子类根本没有继承父类的静态方法。

但是在 ES6 的实现里,我们用同样的语句,会返回 true ,功能正常可用。这说明了 ES6 的 extends 会自动处理静态方法的继承,稍后我会详细解释这一点。

为什么 ES5 时期的人们不注意处理静态方法继承呢?

  1. 核心需求不在此:实例继承才是首要矛盾
    在传统的面向对象编程中,继承最核心、最普遍的场景是基于实例的继承。 Hero.call(this, …) 和 Object.create(Hero.prototype) 这两步操作,完美地解决了这两个核心问题。对于 90% 以上的日常开发场景来说,这就已经足够了。
    静态方法(或静态属性)的使用场景相对要少得多。即使要用,也可以通过直接调用 Parent.staticMethod() 来解决,而不一定非要让 Child 也“继承”它
  2. 实现成本和复杂度巨大
    要在 ES5 时期实现静态方法的继承,需要执行 Object.setPrototypeOf(Child, Parent) 或者非标准的 Child.__proto__ = Parent
    前者因为性能问题被人们诟病(程序中间运行它,就像拦下一辆全速行驶的赛车然后给它换个发动机一样,极大影响性能优化)。而后者又是一种非标准的做法。
    为了一个相对次要的功能,付出如此巨大的代价,自然不会有人愿意做。
  3. “类”的心智模型在 ES5 时代相对模糊
    大家还是把它看成一个函数在用,所以只关注实现了啥功能。这个在 ES6 时期的 class 之后才有所改变。

extends:双重继承的“魔法”

extends 关键字不仅仅是 Object.create() 的语法糖。它完成了两件事

  1. 继承父类的原型(实例方法的继承)
    这部分和 ES5 手写类似,extends 会自动将子类的 prototype 的 [[Prototype]](也就是 proto)指向父类的 prototype,以此完成原型链的链接。
    • 等价实现:Child.prototype.__proto__ = Parent.prototype
    • 查找路径:实例 → Child.prototype → Parent.prototype
    1
    2
    3
    4
    5
    6
    7
    // extends 自动完成了这一步
    Object.setPrototypeOf(Child.prototype, Parent.prototype);
    // 等价于这样写
    Child.prototype.__proto__ = Parent.prototype

    // 这与 ES5 时期的这段代码效果相似,区别在于 ES5 时期的这个需要修复 constructor
    Child.prototype = Object.create(Parent.prototype)
extends 关键字是怎么处理 ES5 时期的 constructor 问题的?

  • ES5 时期的问题:
    Object.create() 方法是破坏 constructor 的原罪,但是也确实是 ES5 时期不得不采用的办法。
    值得一提的是,Child.prototype.__proto__ = Parent.prototype 几乎可以完美解决原型链继承,而且还不用修复 constructor ,看起来是 ES5 时期优于 Object.create() 的办法,但是由于 __proto__ 是非标准用法,这就像黑魔法一样应该避免使用。
    Object.setPrototypeOf() 相当于是 Child.prototype.__proto__ = Parent.prototype 的标准用法,但是这个方法 ES5 时期是没有的,而且由于性能和心智模型的问题所以也没被采用。
  • ES6 时期的优解:
    extends 关键字本质上用了 Object.setPrototypeOf() 方法,但是由于在使用 extends 关键字的时候都是初始的定义部分,所以不用担心“半路拦赛车换发动机”的问题,这就像刚出厂的时候就给赛车的发动机换好了,而不是在它全速奔跑的时候拦截下来换个发动机,那样做会极大影响程序性能。
  1. 继承父类的静态内容(静态方法和静态属性的继承)
    ES5 手写继承时通常不会做这一点。extends 还会将子类构造函数本身的 [[Prototype]] 指向父类构造函数,即 Modal_plus.__proto__ 指向 Modal 。这使得子类可以继承父类的静态方法。
    • 等价实现:Child.__proto__ = Parent
    • 查找路径:Child构造函数 → Parent构造函数

super 关键字

super 是一个非常特殊的关键字,它有两种主要用法,代表着不同的含义:

用法一:作为函数调用 super(…)

示例:

1
2
3
4
5
constructor(title, content, theme) {
// 这条 super 语句一定要写在 this 语句的前面
super(title, content)
this.theme = theme
}
  • 在哪里用? 只能在子类的 constructor 构造函数中。

  • 做什么? 它直接调用父类的 constructor。

  • 等价于什么? super(title, content) 几乎完全等价于 Parent.call(this, title, content),也就是把父亲的 this 赋值语句拿过来执行一遍,但是是用自己的 this 环境。如此一来,子类就也能有父亲的那些属性了。

它的作用就是“构造函数借用”,把子类实例创建时需要的属性的初始化工作,委托给父类的构造函数去完成。

必须先调用 super(),然后才能使用 this

在子类的 constructor 中,必须先调用 super(),然后才能使用 this
因为子类自己没有 this,它的 this 是继承自父类的。必须先通过 super() 让父类把 this 对象创建并初始化好,子类才能接着使用它来添加自己的属性

  • 扩展:为什么呢?
    在 ES5 模式中,子类先创建自己的 this ,然后用 call 语句使父类修饰 this,然后子类再继续修饰 this。但是简单的 call 无法正确初始化父类该有的一些内部状态。
    而 ES6 中,子类自己没有 this ,完全是靠父类创建并返回 this,然后子类继续修饰。这样能确保创建出来的实例具备父类完备的功能且正确初始化。

用法二:作为对象使用 super. methodName()

示例:

在父类里已经写过 show 方法,这里我们用 super 重写父类的方法

1
2
3
4
5
6
7
8
9
10
class Modal_plus extends Modal {
// ...

show(){
super.show()
console.log(`而且主题为 ${this.theme}`);
}

// ...
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
const modal = new Modal('普通弹窗','我是普通弹窗')

const modal_plus = new Modal_plus('主题弹窗','我是主题弹窗','atom dark')

// 输出:
// title 为 普通弹窗 的模态框显示了
modal.show()

// 输出:
// title 为 主题弹窗 的模态框显示了
// 而且主题为 atom dark
modal_plus.show()
  • 在哪里用? 在子类的普通方法中。

  • 做什么? 用来调用父类原型上的方法。

  • 这有什么用? 当你想在子类中重写(Override) 父类的方法,但又想在新的方法里保留父类的原有功能时,它就非常有用了。

写到这里,作为学过 java 的我不禁感叹,完全符合我对面向对象的猜测,因为我猜测它该有这个功能,学着学着发现还真有。

  • 标题: ES6 面向对象编程的语法糖
  • 作者: 三葉Leaves
  • 创建于 : 2025-07-22 00:00:00
  • 更新于 : 2025-08-15 12:09:51
  • 链接: https://blog.oksanye.com/9487a151ca4c/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
来发评论吧~
Powered by Waline v3.2.2