深入了解JavaScript 私有化
class field declarations for javascript(javascript 类的字段声明)目前已经进入了 stage-3,其中包含一项 oop 开发者都很关注的内容:private fields。javascript 一直没有私有成员并不是没有原因,所以这一提议给 javascript 带来了新的挑战。但同时,javascript 在 es2015 发布的时候已经在考虑私有化的问题了,所以要实现私有成员也并非毫无基础。
坑
首先挖个坑 —— 这是一段 js 代码,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"); } }
上面这两段代码是很典型的基于继承的 oop 思想,本意是期望各个层次的类都可以通过 layout() 来进行各层次应该负责的布局任务。但理想和现实总是有差距的,在 javascript 中运行就会发现 businessview._layoutmap() 被执行了两次,而 mapview._layoutmap() 未执行。为什么?
虚函数
javascript 中如果在祖先和子孙类中定义了相同的名称的方法,默认会调用子孙类中的这个方法。如果想调用祖先类中的同名方法,需要在子孙类中通过 super. 来调用。
这里可以分析一下这个过程:
在子类创建对象的时候,其类和所有祖先类的定义都已经加载了。这个时候
- 调用 businessview.layout()
- 找到 super.layout(),开始调用 mapview.layout()
- mapview.layout() 中调用this._layoutmap()
- 于是从当前对象(businessview 对象)寻找 _layoutmap()
- 找到,调用它
你看,由于 businessview 定义了 _layoutmap,所以压根都没去搜索原型链。对的,这是基于原型关系的 oop 的局限。如果我们看看 c# 的处理过程,就会发现有所不同
- 调用 businessview.layout()
- 找到 base.layout(),开始调用 mapview.layout()
- mapview.layout() 中调用 this._layoutmap()
- 在 mapview 中找到 _layoutmap()
- 检查是否虚函数
- 如果是,往子类找到最后一个重载(override)函数,调用
- 如果不是,直接调用
发现区别了吗?关键是在于判断“虚函数”。
然而,这跟私有成员又有什么关系呢?因为私有函数肯定不是虚函数,所以在 c# 中,如果将 _layoutmap 定义为私有,那 mapview.layout() 调用的就一定是 mapview._layoutmap()。
虚函数的概念有点小复杂。不过可以简单理解为,如果一个成员方法被声明为虚函数,在调用的时候就会延着其虚函数链找到最后的重载来进行调用。
javascript 中虽然约定 _ 前缀的是私有,那也只是君子之约,它实质上仍然不是私有。君子之约对人有效,计算机又不知道你有这个约定……。但是,如果 javascript 真的实现了私有成员,那么计算机就知道了,_layoutmap() 是个私有方法,应该调用本类中的定义,而不是去寻找子类中的定义。
解决当下的私有化问题
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 出现的理由之一就是解决私有化问题,但是为什么要用以及怎么用就需要去分析和思考了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。