Class field declarations for JavaScript(JavaScript 类的字段声明)目前已经进入了 stage-3,其中包含一项 OOP 开发者都很关注的内容:Private fields。JavaScript 一直没有私有成员并非没有缘由,因此这一提议给 JavaScript 带来了新的挑战。但同时,JavaScript 在 ES2015 发布的时候已经在考虑私有化的问题了,因此要实现私有成员也并不是毫无基础。闭包
笔者在专栏《JavaScript 全栈工程师养成记》的第四章讲到了原型 OOP 关系和继承 OOP 关系的关键区别。今天这里就研究一下 JavaScript 私有成员的问题。ide
首先挖个坑 —— 这是一段 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.
来调用。this
这里能够分析一下这个过程:code
在子类建立对象的时候,其类和全部祖先类的定义都已经加载了。这个时候orm
BusinessView.layout()
super.layout()
,开始调用 MapView.layout()
MapView.layout()
中调用this._layoutMap()
对象
BusinessView
对象)寻找 _layoutMap()
你看,因为 BusinessView
定义了 _layoutMap
,因此压根都没去搜索原型链。对的,这是基于原型关系的 OOP 的局限。若是咱们看看 C# 的处理过程,就会发现有所不一样blog
BusinessView.layout()
base.layout()
,开始调用 MapView.layout()
MapView.layout()
中调用 this._layoutMap()
MapView
中找到 _layoutMap()
检查是否虚函数
发现区别了吗?关键是在于判断“虚函数”。
然而,这跟私有成员又有什么关系呢?由于私有函数确定不是虚函数,因此在 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
出现的理由之一就是解决私有化问题,可是为何要用以及怎么用就须要去分析和思考了。
学习可让人解决相同的问题,但思考可让人解决类似的问题。欢迎读者们来学习笔者的专栏《JavaScript 全栈工程师养成记》,并跟着笔者一块儿思考、分析和解决软件开发过程当中的若干问题。