私有属性的实现
栏目: 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 私有属性的草案
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python金融衍生品大数据分析:建模、模拟、校准与对冲
【德】Yves Hilpisch(伊夫·希尔皮斯科) / 蔡立耑 / 电子工业出版社 / 2017-8 / 99.00
Python 在衍生工具分析领域占据重要地位,使机构能够快速、有效地提供定价、交易及风险管理的结果。《Python金融衍生品大数据分析:建模、模拟、校准与对冲》精心介绍了有效定价期权的四个领域:基于巿场定价的过程、完善的巿场模型、数值方法及技术。书中的内容分为三个部分。第一部分着眼于影响股指期权价值的风险,以及股票和利率的相关实证发现。第二部分包括套利定价理论、离散及连续时间的风险中性定价,并介绍......一起来看看 《Python金融衍生品大数据分析:建模、模拟、校准与对冲》 这本书的介绍吧!