私有属性的实现
栏目: JavaScript · 发布时间: 6年前
内容简介:目前,类与对象都存在两种性质的成员,一是属性,二是方法。——尽管在ES6以前“函数类型的属性”也被称为方法,但在ES6及其以后,它们不再是严格意义上的方法了。后者,亦即是ES6+的方法也是一种“(特殊的)函数类型的属性”,这种特殊性在于它必须通过声明语法来添加到类或对象。——也就是说,ES6中的方法是静态声明的,而不是动态添加的。类是特定语法声明的函数。由于函数是对象,所以类也是普遍含义上的对象。所以——重要的是——类的成员与对象的成员在性质上并没有任何的不同。确切来讲,它们都是属性,是对象的自有属性表中
在tc39的提案中,这一特性被称为"private field",据说是为了避免与property这个传统的名字出现概念冲突。这简直是扎了裤脚放屁——还要称比脱了裤子文雅!为什么这么讲呢?因为一旦引入了所谓“private filed”,就预示着还会有“public field”等等之类,而这与传统的property又有什么不同吗?
还是叫“私有属性(private property)”吧,别再出妖了。
本文是一个系列,参见:
- No prefix! operator is Ok! -在这里
- 私有属性的实现 (本文)
- (未完待续)
1. 私有成员语法的三个问题
目前,类与对象都存在两种性质的成员,一是属性,二是方法。——尽管在ES6以前“函数类型的属性”也被称为方法,但在ES6及其以后,它们不再是严格意义上的方法了。后者,亦即是ES6+的方法也是一种“(特殊的)函数类型的属性”,这种特殊性在于它必须通过声明语法来添加到类或对象。——也就是说,ES6中的方法是静态声明的,而不是动态添加的。
类是特定语法声明的函数。由于函数是对象,所以类也是普遍含义上的对象。所以——重要的是——类的成员与对象的成员在性质上并没有任何的不同。确切来讲,它们都是属性,是对象的自有属性表中的成员,或可以通过原型访问的父代类属性。
总之,所有现在能静态声明或动态添加的属性都是公开的(public)。在讨论“私有属性”的时候,有三点是必须要先确定的:
- 私有属性是静态声明的,还是动态添加的?
- 私有属性是(现有机制中的)自有属性表中的“标识为私有的”属性,还是在一个新表?
- 私有属性是否支持继承(类似其它OOP语言的protected属性)?
注:本文中所谓“成员”,是在需要区分讨论属性与方法时使用的。此外,在提及“词法私有成员”时,因其与“属性”存在本质不同,所以暂用了“成员”这个概念。
2. tc39中的提案
之前我们已经讨论过这个提案的语法。很大程度上我是在尽量保证与现有语法设计上的一致性,而并没有讨论这个提案的实现方法。
简单地说,该提案认为:私有属性是一种特殊前缀表达的、自有的特殊属性。
由于私有属性是“一种…属性”,因此该属性总是在“自有属性表”中,并且也受自有属性表的机制来约束,例如属性描述符。比较简单的处理方法是:在属性描述符中增加新的属性(类似 configurable
与 enumerable
等等)。因此:
class f() { private data: 100 } x = f(); // 在语义上等义于: Object.defineProperty(x, 'data', { value: 100, kind: 'private' // default is 'public' })
所有与“属性”相关的特性都可以用在这些新式的 kind: 'private'
的私有属性上。当然,如此一来,我们之前讨论的三个问题就有答案了:
-
私有属性是静态声明的,不可以动态添加。
私有属性不支持用Object.defineProperty()等来动态声明,这可以通过
kind
性质来限制之。 -
私有属性使用可以使用一个新的属性表,也可以直接使用现有的自有属性表。
两者的机制是完全一样的,只是
kind
性质的不同。但是,无论哪种方法,都需要考虑公开与私有属性同名问题:是可以同时存在,还是禁止重复。(注1、注2) -
私有属性可以支持继承。
在这个方案中可以很方便的实现类似
protected
的语法,即在子类中可访问的父类私有属性(如果父类将之声明为protected)。
注1:建议允许重名。因为JavaScript允许动态添加公开属性名的,如果不允许重名,则发生与私有属性冲突的机率大增。——更严重的是,用户代码无从得知一个对象有哪些私有属性名。
注2:如果允许重名,则两张属性表更方便;如果禁止重名,则建议使用同一张表。
如上,我的确是建议“私有的与公开的属性名是可以重复的”。这带来了如下的问题(下例假设直接使用 this.xxx
来存取私有属性):
// 示例:一个不太可行的方案 class f{ data = 100; // 假设这里是私有成员 foo() { console.log(this.data); // 假设这里的this.data指向私有成员 } } x = new f; x.data = '200'; f(); // 应该显示200还是100?
这一示例说明:我们无法在语法上“限定在 class f()
类声明中使用 this.data
将访问私有属性。这个设计是行不通的。
如果我们的假设是“私有属性是属性表中的特殊项”,那么“使用 this
来访问属性表”是目前最合理的方式。又如果使用“ this
作为绑定给 foo()
的对象实例,并试图访问该实例的私有属性”,那么“必然的”,我们需要一个新的语法:用于存取私有属性。
——注意这与目前tc39提案的不同,亦即是 “No prefix! operator is Ok!” 。
如下例:
class f() { private data: 100, private static data: 200, foo() { this#data = this#data + f#data; } }
在这个设计中, #
是限用于“类和对象的方法声明语法中”的,语义是:
- 存取左运算元的私有属性表
很简单,很明确。
3. 使用属性表的核心问题
我们使用 pravite
作为限定词,核心目的是“访问在 class f()
声明之外访问私有属性。然而,如果我们使用属性表,那么没有任何”经济的“方法能做实现这个目标。为了解决这个问题,我们先讨论一个ECMAScript规范上的漏洞: 能不能动态添加方法声明?
3.1 动态添加方法
JavaScript不能安全地抄写对象方法,因为方法会绑定源对象的 super
。例如:。
// 类f与对象x ObjectX = function() {} ObjectX.prototype.data = "ObjectX" class f extends ObjectX { } f.prototype.data = 100; x = new f; // 类f2与对象x2 MyObject = function() {} MyObject.prototype.data = 'MyObject'; class f2 extends MyObject { foo() { console.log(super.data); console.log(this.data); } } f2.prototype.data = 200; x2 = new f2; x2.foo(); // "MyObject" 200 // 抄写x2的方法声明 // (super没变化,绑定在了f2()类上) x.foo = x2.foo; x.foo(); // "MyObject" 100
方法 x.foo()
显示了 foo()
在词法上的 super
,亦即是 MyObject
。这表明 foo()
方法是与 class f2
的继承关系是词法绑定的,并不会“抄写到”对象 x
。——当然,如我们一直在讨论的, this
引用是动态绑定的,不受上述影响。
而所谓 super
本质上只是在访问一个方法的“内部槽 [[HomeObject]]
”的原型而已。考虑到这一点,一种简单的重置 super
的方法如下:
// 安全地动态添加方法x.foo() Object.assign(x, Object.setPrototypeOf({ foo() { console.log(super.data); console.log(this.data); } }, Object.getPrototypeOf(f.prototype))); x.foo(); // "ObjectX", "100"
根据这一过程,一个安全地动态添加方法的操作如下(注3):
// 工具函数 // - add method to f.prototype Object.addMethod = function(proto, methods) { return Object.assign(proto, Object.setPrototypeOf(methods, Object.getPrototypeOf(proto))); } // 使用示例(在原型上添加方法) Object.addMethod(f.prototype, { foo2() { console.log('Value:', super.data); } }) x.foo2(); // "Value: ObjectX"
注3: Object.addMethod()
在这里实现为“向f.prototype属性添加方法”,是因为通过 class
关键字声明的对象方法其实都是原型方法。因此所谓“动态添加方法”是应当通过污染类的.prototype属性来实现的。但是也可以用类似技术来实现不污染该原型属性的方法,例如 Object.addOwnMethod()
,因与本文主题无关,在这里就不给出具体代码了。
3.2 动态存取私有属性
Object.addMethod()
方法的实现意味着“在类声明之外”为对象或类动态添加方法是安全的。因此,尽管我们认为下面的私有属性this#data 只能通过
foo()`来访问:
class f { private data: 100, foo() { console.log(this#data); } } x = new f; x.foo();
然而事实上下面的示例可以随时访问之:
Object.addMethod(f.prototype, { setData(v) { this#data = v; } }) // setData()是动态添加的可以存取私有属性的方法 x.setData(300); x.foo(); // 300
然而这样一来,在ECMAScript中没有办法“ 安全地实现私有属性 ”了。
4. 存在可行的解决方案吗?
首先我们得知道为什么。
ECMAScript的方法本质上是“访问 this
绑定的函数”——无论是传统的方法,还是ES6之后的方法,皆是如此。在所谓方法中,如果访问的是 this.xxx
这样的公共属性自然是不成问题,因为本质上对象就是属性表;类似的,如果属性包支持“ private
这样的访问域特性”,那么自然也是可以在支持私有属性访问的。——所以,最新的提案是建议用 this.#xxx
这样的语法来访问它,亦即是在本质上仍然是在访问属性表。
所以,这就是根本的问题:如果一个方法就是在访问属性表,且它总是能动态地绑定 this
,那么它必然也能够跨类(和跨对象)地访问私有的属性表——以及那些私有属性。
有两个途径来解决这个问题。
其一,是采用“词法私有成员”这样一种潜在实现机制。由于这种实现机制是利用作用域来实现的,与对象的自有属性表无关,因此也不会受 addMethod()
这样的方法影响 。
其二,是在方法声明(例如 f.prototype.foo()
)中对 this#xxx
中的运算符 #
做进一步限制。即foo()方法
-
仅能在
f.prototype
与this对象的原型
相同时(即private
限定词),以及 -
仅能在
f.prototype
在this对象的原型
的原型链上时(即protected
限定词),
才能使用 #
运算。而这一点恰恰是能做到的,因为——正好——每一个ES6之后的方法,都会有一个 [[HomeObject]]
内部槽用于存放**“声明时的”**类原型( f.prototype
)或对象本身( obj
)。如下例:
class f { foo() {} } obj = { foo() {} } x = new f; // 如果能访问[[HomeObject]]内部槽,则: console.log(x.foo[[HomeObject]] === f.prototype); // true console.log(obj.foo[[HomeObject]] === obj); // true
那么,上述对 #
运算符的限制无非是在说(注4、注5):
// 在foo()方法内实现`#`运算符的伪代码逻辑 foo() { // source: `this#data` homeObject = activeFunction[[HomeObject]]; descriptor = Object.getPrivatePropertyDescriptor(this, 'data'); permission = ( (descriptor.kind == 'private' && homeObject == Object.getPrototypeOf(this)) || (descriptor.kind == 'protected' && homeObject.isPrototypeOf(this))); if (permission) { result = descriptor.value; // return result of `#` op } }
注意这里有一个伪代码说明的变量名 activeFunction
,这是指“当前活动的、正在调用的函数,也就是 foo()
函数本身。类似在ECMAScript中“需要判断当前函数”的情况并不是没有——也就是说,以前就有存在这样这种方案实现的语言特性了。
是哪种语言特性呢?这就是闻名已久的 super()
——注意这里仅指在 constructor()
调用父类构造方法的操作,在ECMAScript中称为 SuperCall
。这个操作很特殊,因为它需要从当前上下文的环境记录中取 activeFunction
并进一步“查找所谓父类(即super)”。
注4: 在ECMAScript中有两个操作是会取activeFunction的,另一个是eval()。 SuperCall()
需要取activeFunction的原因是它无法直接使用constructor()的[[HomeObject]]内部槽。
注5: Object.getPrivatePropertyDescriptor()
与 Object.getOwnPropertyDescriptor()
并没有功能的区别,只是这里假设前者需要直接访问私有属性表(关于建议使用两个属性表的问题,参见本文第2节)。
亦即是说,所有实现这一特性所需的技术组件,在现有的ECMAScript中都是充备的。
^^.
n. 终极问题:必要性?
看起来还不错哟。看起来很有希望哟。要不,下手来搞搞?
然而,历史中,我们 程序员 犯得最多的错误就是:
一件事看起来能做,于是就做了。
“私有属性”这个特性真的是必须的么?——常读我写文章的读者,应该知道我总是在最后一步来考虑最原始的问题。——然而关于这个问题,我想要等到下一篇再讨论了。
现在这篇,已经很长了,不能再长了。:smiley:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 协议中的私有属性
- JavaScript 新语法详解:Class 的私有属性与私有方法
- 外部调用类的私有属性
- [译] ECMAScript 类 —— 定义私有属性
- JavaScript Class(类) 中的 Private(私有) 和 Public(公有) 属性
- TC39 在 GitHub 通过一条 EMCAScript 私有属性的草案
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First Python
Paul Barry / O'Reilly Media / 2010-11-30 / USD 49.99
Are you keen to add Python to your programming skills? Learn quickly and have some fun at the same time with Head First Python. This book takes you beyond typical how-to manuals with engaging images, ......一起来看看 《Head First Python》 这本书的介绍吧!