JavaScript 中类的私有与保护成员
JavaScript 是基于对象的语言,但是自始至终,它的类与对象的概念都与以 C++ 为代表的编程语言有较大的区别。class
虽然从最初开始就是关键词,但是也是在最近的 ES6
中才被具体定义的,而关于类成员的访问控制在语法层面则是完全缺失的。从 JavaScript 开始应用于构建复杂交互起,对于如何模拟实现类成员的访问控制就一直是个老生常谈的问题,本文就尝试总结下目前想到的所有可能的模拟方法。
需要特别说明的是,不具备成员访问控制实际不会对应用有多大的影响,大家早已习惯,所有的这些模拟方法都只是代码层面的奇技淫巧而已,并不具备多大的实际使用价值。而真正意义上的 private fields 目前已处于草案阶段(Stage 2),离语法层面支持成员访问控制的日子不远了。
下划线
这个不是具体实现的方法,而是一种普遍的共识、约定俗成的规定:私有/保护成员以下划线开头进行命名。它比后面介绍的所有方法都要有效与简洁,并且借助于静态代码检测工具基本也能保证规则的强制性。
利用闭包
在 JavaScript 流行的初期,有太多针对 JavaScript 另类的类设计思路的抱怨,为此道格拉斯(Douglas Crockford)大神多次澄清,JavaScript 也是有私有成员的,只是和你们想要的不一样罢了,具体的实现就是使用闭包:
1 | function Human(age) { |
其中 _age
就是私有变量,只存在于构造函数 Human
中,除了可以使用 getAge
方法访问外别无他法,妥妥的私有属性。
这种方式不好的地方在于书写不好看(认真脸)… 所有的私有成员及其需要访问私有成员的方法都必须书写在构造函数中,必定导致构造函数过于臃肿(划重点:超过 50 行的函数都是耍流氓)。当然另外一个教科书式的理由是方法都直接写到对象上了,你让 prototype
情何以堪?如果存在继承的情况,子类想要覆盖这些方法都又要废一番功夫。
最后这个方法只能实现私有成员,无法实现保护成员 … >_<
利用 WeakMap
ES6
中的 WeakMap
可以使用对象作为 key
而且是弱引用,不会造成内存泄漏,可以帮助实现私有成员:
1 | let privated = new WeakMap(); |
类的私有成员并不存储在类中,而是保存在一个独立的 WeakMap
中,使用对象自身作为 Key
来存储需要的数据,只要这个 WeakMap
不暴露就能保证数据的私有性。
书写上比闭包的方式好多了,是 constructor
的归 constructor
,是 prototype
的归 prototype
,实现简单,唯一的不足就是存取私有成员比较繁琐。除了实现私有成员外,这种方式也能实现保护成员,只要将多个类写在同一个模块中使之能共享同一个 WeakMap
就好,不过这样一来也就破坏了一个模块一个类的单一原则,所以还是只考虑用来实现私有成员的好。
利用 Symbol
只要私有成员不能在类外部被访问到,具体把这些成员存储到哪里都不是问题,甚至就直接放到对象上也OK,只是对应的 Key
让人猜不到就好。利用 Symbol
实现私有成员就是这种方式:
1 | const KEY_AGE = Symbol('age'); |
这个属性对应的 Key
是一个只在类 Human
所在的模块内才能被访问的 Symbol
对象,外部无从知晓这个 Key
,也就无从访问这个属性,自然也就是私有的了。实际上用不用 Symbol
都行,只要隐藏好 Key
就成,不过上面的实现并不完整,虽然外部用户无法访问到这个 Key
,但是能通过 Object.getOwnPropertyNames
、getOwnPropertySymbols
之类的方法遍历得到这些 “隐藏” 的 Key
,要实现真正意义上的 “隐藏” Key
还需要借助 Proxy
的帮助。
另外与利用 WeakMap
的形式相同,虽然这种方式也能用于实现保护成员,但同样也会违背模块的单一原则。
利用 Proxy
作为元编程(Metaprogramming) 的一种手段,Proxy
可以动态的改变对象或者函数的数据结构和行为,通过它可以实现标准的保护成员。核心思路是控制对象的 Get
与 Set
行为,在具体操作前进行检查,如果需要访问的成员属于保护成员就阻止访问,比如这样(以下示例都只以控制对象成员的获取为例):
1 | function protectObject(obj) { |
如此一来 human
的 _age
就永远无法访问了,啊哈哈 …… 且慢,只是这样的话就真的是完全无法访问,我们想要的是保护成员,在类及其子类中可以访问,外部不能访问,而现在是谁都不能访问… 所以除了从名称上判断访问的成员类型外还要判断访问的路径,是从外部访问的还是从内部访问的?内部访问也就是从类及子类的方法中发起的访问,所以还需要更进一步的对类各个方法的调用进行记录,以便确定访问的路径:
1 | function protectObject(obj) { |
呃,代码瞬间膨胀了… 好在使用上还算简单:
1 | class Human { |
继承的情况:
1 | class Man extends Human { |
再辅以装饰器(decorators)的配合,perfect ~:
1 | function protect(target) { |
这种方式在使用上最自然、简洁,感觉上是最好的方式(性能方面还未考察…),不过也只适用于模拟保护成员,对于私有成员就束手无策了。
One more thing … 那啥… 这里的实现有漏洞(我才不说是 Bug 呢),并不能完全保证保护成员不被外部访问,比如这样:
1 | class Crash extends Human { |
漏洞出在 protectObject
中判断是否是内部方法发起的访问,目前的判断方式只是近似求解,对于内部方法调用回调参数是无法正确判断的… 更优的方案暂时没想到,回头想到了再补充吧…
总结
还是那句话,目前这些方法都是徒劳,想要访问控制还是乖乖等规范吧 ~