ES6 面向对象编程的语法糖

书接上文: 继承的手写实现
既然我们已经搞明白继承的原理了,下面我们就可以看看 ES6 里是怎么做的了。
还是那个模态框的例子,这次我们使用 ES6 的方式实现
1 | class Modal { |
单从代码量上看,就简洁的不止一点了。
关于静态方法的继承
看懂很简单,但是底层的细节还是值得深挖的。先说一下功能上的一个显著不同点:
在之前,对于挂在构造函数上的静态方法,ES5 的方式子类并不会继承父类的静态方法。
1 | // 🌟父类的定义部分加上这一条:定义父类的静态方法 |
这在 ES6 里是用 static 关键字写在构造函数里的。关于静态方法、实例方法、对象自带方法的区别,可见 原型对象与方法
定义好以后,我们来到测试部分,让子类试图调用父类的这个静态方法:
1 | console.log(Modal_plus.isModal(modal)); |
会报错:
1 | Uncaught TypeError: Modal_plus.isModal is not a function |
这说明了子类根本没有继承父类的静态方法。
但是在 ES6 的实现里,我们用同样的语句,会返回 true
,功能正常可用。这说明了 ES6 的 extends 会自动处理静态方法的继承,稍后我会详细解释这一点。
为什么 ES5 时期的人们不注意处理静态方法继承呢?
- 核心需求不在此:实例继承才是首要矛盾。
在传统的面向对象编程中,继承最核心、最普遍的场景是基于实例的继承。 Hero.call(this, …) 和 Object.create(Hero.prototype) 这两步操作,完美地解决了这两个核心问题。对于 90% 以上的日常开发场景来说,这就已经足够了。
静态方法(或静态属性)的使用场景相对要少得多。即使要用,也可以通过直接调用 Parent.staticMethod() 来解决,而不一定非要让 Child 也“继承”它 - 实现成本和复杂度巨大
要在 ES5 时期实现静态方法的继承,需要执行 Object.setPrototypeOf(Child, Parent) 或者非标准的Child.__proto__ = Parent
。
前者因为性能问题被人们诟病(程序中间运行它,就像拦下一辆全速行驶的赛车然后给它换个发动机一样,极大影响性能优化)。而后者又是一种非标准的做法。
为了一个相对次要的功能,付出如此巨大的代价,自然不会有人愿意做。 - “类”的心智模型在 ES5 时代相对模糊
大家还是把它看成一个函数在用,所以只关注实现了啥功能。这个在 ES6 时期的 class 之后才有所改变。
extends:双重继承的“魔法”
extends 关键字不仅仅是 Object.create() 的语法糖。它完成了两件事:
- 继承父类的原型(实例方法的继承):
这部分和 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 关键字的时候都是初始的定义部分,所以不用担心“半路拦赛车换发动机”的问题,这就像刚出厂的时候就给赛车的发动机换好了,而不是在它全速奔跑的时候拦截下来换个发动机,那样做会极大影响程序性能。
- 继承父类的静态内容(静态方法和静态属性的继承):
ES5 手写继承时通常不会做这一点。extends 还会将子类构造函数本身的 [[Prototype]] 指向父类构造函数,即Modal_plus.__proto__
指向Modal
。这使得子类可以继承父类的静态方法。- 等价实现:
Child.__proto__ = Parent
- 查找路径:
Child构造函数 → Parent构造函数
- 等价实现:
super 关键字
super 是一个非常特殊的关键字,它有两种主要用法,代表着不同的含义:
用法一:作为函数调用 super(…)
示例:
1 | constructor(title, content, theme) { |
-
在哪里用? 只能在子类的 constructor 构造函数中。
-
做什么? 它直接调用父类的 constructor。
-
等价于什么?
super(title, content)
几乎完全等价于Parent.call(this, title, content)
,也就是把父亲的 this 赋值语句拿过来执行一遍,但是是用自己的 this 环境。如此一来,子类就也能有父亲的那些属性了。
它的作用就是“构造函数借用”,把子类实例创建时需要的属性的初始化工作,委托给父类的构造函数去完成。
在子类的 constructor 中,必须先调用 super(),然后才能使用 this。
因为子类自己没有 this,它的 this 是继承自父类的。必须先通过 super() 让父类把 this 对象创建并初始化好,子类才能接着使用它来添加自己的属性
- 扩展:为什么呢?
在 ES5 模式中,子类先创建自己的 this ,然后用 call 语句使父类修饰 this,然后子类再继续修饰 this。但是简单的 call 无法正确初始化父类该有的一些内部状态。
而 ES6 中,子类自己没有 this ,完全是靠父类创建并返回 this,然后子类继续修饰。这样能确保创建出来的实例具备父类完备的功能且正确初始化。
用法二:作为对象使用 super. methodName()
示例:
在父类里已经写过 show 方法,这里我们用 super 重写父类的方法
1 | class Modal_plus extends Modal { |
测试:
1 | const modal = new Modal('普通弹窗','我是普通弹窗') |
-
在哪里用? 在子类的普通方法中。
-
做什么? 用来调用父类原型上的方法。
-
这有什么用? 当你想在子类中重写(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 进行许可。
预览: