本文探讨 JavaScript 私有成员的实现机制。针对传统下划线命名约定在继承中失效的问题,深入分析原型链查找导致的调用冲突。通过结合 ES2015 的 Symbol 与闭包或模块作用域,确保私有方法不被子类覆盖,从而在语法层面实现真正的私有化效果。
JavaScript 私有成员背景
JavaScript 类的字段声明提案已进入 stage-3 阶段,其中最受面向对象开发者关注的特性是私有字段(Private fields)。JavaScript 长期缺乏原生私有成员并非没有缘由,该提案为语言引入了新的挑战。不过,自 ES2015 发布以来,语言设计者已在思考私有化问题,因此实现私有成员具备一定的演进基础。
继承中的私有陷阱
潜在陷阱
首先通过一个案例引入问题:在 BusinessView 类中,需要执行两项布局操作,分别针对表单和地图。
业界通常约定以 _ 前缀标识私有成员。
class BaseView {
layout() {
console.log("BaseView Layout");
}
}
class BusinessView extends BaseView {
layout() {
super.layout();
this._layoutForm();
this._layoutMap();
}
_layoutForm() {
// ....
}
_layoutMap() {
// ....
}
}
随着业务发展,发现多个视图包含地图布局逻辑。为此采用继承方式,将地图相关功能从 BusinessView 抽象为基类 MapView。
class MapView extends BaseView {
layout() {
super.layout();
this._layoutMap();
}
_layoutMap() {
console.log("MapView layout map");
}
}
class BusinessView extends MapView {
layout() {
super.layout();
this._layoutForm();
this._layoutMap();
}
_layoutForm() {
// ....
}
_layoutMap() {
console.log("BusinessView layout map");
}
}
上述代码体现了典型的基于继承的面向对象思想,旨在通过 layout() 方法让各层级类负责各自的布局任务。然而实际运行 JavaScript 代码时会出现异常:BusinessView._layoutMap() 被执行了两次,而 MapView._layoutMap() 从未执行。原因何在?
虚函数机制分析
虚函数机制
在 JavaScript 中,若祖先类与子孙类定义了同名方法,默认调用子孙类中的方法。若需调用祖先类的同名方法,必须在子孙类中通过 super. 显式调用。
以下是对该执行过程的分析:
在子类创建对象时,其自身及所有祖先类的定义均已加载。此时:
- 调用 BusinessView.layout()
- 解析到 super.layout(),开始调用 MapView.layout()
- MapView.layout() 中调用 this._layoutMap()
- 从当前对象(BusinessView 实例)中查找 _layoutMap()
- 找到匹配项,并调用该方法
由于 BusinessView 定义了 _layoutMap,JavaScript 引擎直接调用它,并未继续搜索原型链。这揭示了基于原型关系的面向对象编程的局限性。对比 C# 的处理流程,可以发现显著差异:
- 调用 BusinessView.layout()
- 解析到 base.layout(),开始调用 MapView.layout()
- MapView.layout() 中调用 this._layoutMap()
- 在 MapView 中找到 _layoutMap()
- 检查是否虚函数
- 若该成员为虚函数,则沿虚函数链查找最后一个重载(override)方法并调用
- 若非虚函数,则直接调用当前定义
区别的关键在于对“虚函数”的判断机制。
这与私有成员有何关联?在 C# 中,若将 _layoutMap 定义为私有,它就不是虚函数,因此 MapView.layout() 调用时必定执行 MapView._layoutMap()。
虚函数概念较为复杂,但可简化理解为:若成员方法声明为虚函数,调用时会沿虚函数链查找并执行最后的重载版本。
JavaScript 中约定 _ 前缀代表私有,但这仅是开发者的君子协定,实质上该方法仍非私有。计算机无法识别这种约定。若 JavaScript 实现真正的私有成员,引擎会识别 _layoutMap() 为私有方法,从而调用本类定义,而非子类中的覆盖版本。
Symbol 与闭包解决方案
解决当前的私有化方案
尽管 JavaScript 目前缺乏原生私有成员,但需即时有效解决该问题。可通过 Symbol 和闭包来实现。
注意,此处的闭包并非指在函数内部生成闭包,请继续阅读。
首先重新审视私有化问题:目标是使祖先类调用方法时,不再去子类中查找同名方法。语法层面无法直接解决,因为 JavaScript 必须从具体实例由后往前查找指定名称的方法。但如果找不到该方法名呢?
之所以能找到,是因为方法名是字符串,在全局作用域内具有相同含义。ES2015 引入了 Symbol,它必须实例化,且每次实例化代表不同的标识。若将类定义在闭包中,并在闭包内声明一个 Symbol 作为私有成员名称,即可解决问题,例如:
const MapView = (() => {
const _layoutMap = Symbol();
return class MapView extends BaseView {
layout() {
super.layout();
this[_layoutMap]();
}
[_layoutMap]() {
console.log("MapView layout map");
}
}
})();
const BusinessView = (() => {
const _layoutForm = Symbol();
const _layoutMap = Symbol();
return class BusinessView extends MapView {
layout() {
super.layout();
this[_layoutForm]();
this[_layoutMap]();
}
[_layoutForm]() {
// ....
}
[_layoutMap]() {
console.log("BusinessView layout map");
}
}
})();
现代基于模块的定义甚至无需显式闭包(模块系统会自动封闭作用域)。
const _layoutMap = Symbol();
export class MapView extends BaseView {
layout() {
super.layout();
this[_layoutMap]();
}
[_layoutMap]() {
console.log("MapView layout map");
}
}
const _layoutForm = Symbol();
const _layoutMap = Symbol();
export class BusinessView extends MapView {
layout() {
super.layout();
this[_layoutForm]();
this[_layoutMap]();
}
[_layoutForm]() {
// ....
}
[_layoutMap]() {
console.log("BusinessView layout map");
}
}
改革后的代码可按预期输出结果:
BaseView Layout
MapView layout map
BusinessView layout map
总结
笔者在多年开发中养成了分析问题的思维习惯,能迅速透过现象看到实质。Symbol 出现的理由之一正是解决私有化问题,但关键在于如何分析和利用现有条件。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。