本文是系列文章,包括:
- "Field提案"是什么东东 - 在这里
- 关于废止proposal-class-fields提案的建议 - 在这里
- 私有属性的实现 - 在这里
- No prefix! operator is Ok!(本文)
- (未完待续)
本文是对一份tc39提案的讨论。原有提案:https://github.com/tc39/proposal-class-fields
所有在我对原有提案的修改中,核心不是“不用#
字符”,而是将它从一个前缀字符,变成了一个操作符。这一方面是使“声明语法”与“表达式运算”分开,另一方面也让这些修改与ECMAScript规范保持在语法上的一致性。
- 修改的提议:#issuecomment-429533532
1. 为什么是":"而不是"="?
在所有类、对象等声明性质的语法中,":"是表明"key/value pair"的,既然这里的私有字段仍然是“key/value pair”,那么仍然建议使用该符号。而原提案建议使用=
并且与TypeScript保持一致,却忽略了TypeScript中完整的语法x: number = 100
中,:
是指示类型的,而不是用于指示值。——这与ECMAScript的一般规则并不一致。
ECMAScript规范事实上是沿用了“旧式的对象字面量”的属性声明与初值的语法,亦即是:
obj = {
x: 100,
y: 100
}
注意在这个语法中”有或没有,
号都是接受的,但如果有,
号则称为一个List,且整个声明是以一个“没有,
号的单个属性声明”结束的。——这与传统的对象字面量声明语法一致。
在ECMAScript中,类声明一定程度上沿用和扩展了这一语法。一方面,把“方法声明”给到了对象字面量声明;另一方面,从对象字面量那里把“get/setter声明”拿了过来。而与TypeScript类似的= xxxx
语法,尽管也是一个称为Initializer的语法组件,也的确出现在了对象初始化语法中,但是作为错误语法来识别的(CoverInitializedName):
PropertyDefinition: CoverInitializedName
Always throw a Syntax Error if code matches this production.
所以,回到最前面的说明,推荐的语法设计是:
// Because
obj = {
data: 100,
foo() {
....
}
}
// YES
class f {
data: 100,
foo() {
...
}
}
// NO!!!
class f {
data = 100;
...
}
这样使用f()
类构造出来的对象实例,与用相似语法声明的对象字面量是类似的。不存在语法设计上的“例外(unexpected)”。
2. 为什么是"private x"而不是"#"?
在现有类、对象等声明语法中是使用限定词来指示成员性质的,例如static
、get
等。除了生成器函数之外,并不使用限定符号或“前缀(prefix)”,因此我建议用限定词private
来扩展类声明语法,而反对原提案中使用#
作为限定符来声明类私有字段(private field of class)。例如:
// YES
class f {
private data: 100
}
// NO!
class f {
#data = 100;
}
3.为什么是","而不是";"?
在所有的列表(List)中,ECMAScript采用的分隔符都是,
号,例如参数列表、对象/数组成员列表,以及导入导出的名字列表,还有变量var
声明的名字列表等等。而;
在语法中惯例是用作语句结束符或分隔符,包括你所见的各种(任意的)语句,以及for(;;)
中的子句等等。我们既然是在声明类或对象的“成员列表”,那么显然应该是按“列表(List)”的规则处理成,
为好,怎么会用到;
号了呢?
TypeScript中在这个位置是这样声明的:
class f {
data: string = 'in typescript';
...
}
留意这里的语法特点:符号:
是用于指示类型的,因此初值声明使用了=
。这个声明与var
语句声明类似,TypeScript将这里处理成了;
是可以理解的。但ECMAScript何必要绕开现成的,
号不用,去使用在这里毫无意义的;
号呢?
4.最后一个","号的问题
ECMAScript既然已经接受了对象字面量声明的末尾逗号(object literal trailing commas),那么下面两种声明都可以是合法的了:
// 推荐
class f {
data: 100,
foo() {
...
}
}
// (可接受)
class f {
..., // more, a list with ','
data: 100
foo() {
...
}
}
5.其它的声明示例
包括:
var data = 'outer';
class f {
// reference
data, // outer reference, no computed
// public
data: 100, // for object, equ: f.prototype.data = 100
static data: 100, // for class, equ: f.data = 100
["da"+"ta"]: 100, // computed
static ["da"+"ta"]: 100, // computed
// private
private data: 100, // normal private object properties
private ["da"+"ta"]: 100, // computed private object properties, and symbol keys
private static data: 100, // for class static field
private static ["da" + "ta"]: 100, // computed
// get&setter, etc
private get data() { ... }
...
}
6.私有属性的存取
私有属性的存取是一个大问题,也是#
语法争议的焦点。首先必须确定的是:私有属性存取的语义是什么?
在原提案中,私有属性存取是将#
作为一个前缀(prefix),而存取运算仍然是.
或[]
运算符。因此,本质上来说,存取运算的操作没变,但是需要在存取中判断属性名的前缀是否是#
字符。如下例:
// 原提案
class f {
#data = 100;
foo() {
console.log(this.#data); // "#"是前缀,而"."是存取运算符
}
}
根本原因出在.
运算检测data
是否是私有成员的成本过高。例如:
// 示例:一个不太可行的方案
class f{
data = 100; // 假设这里是私有成员
foo() {
console.log(this.data); // 假设这里的this.data指向私有成员
}
}
x = new f;
x.data = '200';
f(); // 应该显示200还是100?
那么在上面这个例子中,x.data
添加了一个公开的属性时,foo()方法是无法识别用户代码的意图的。
所以在旧的提案中需要使用#
前缀。但是,仔细思考这个问题:
- 私有字段列表与自有成员列表必须是同一个吗?
当然不需要。那么为什么不为私有字段列表安排一个专门的运算符呢?只要“像使用super一样”限定它使用的上下文就好。因此,新的语法设计如下:
class f{
data: 100,
foo() {
console.log(this#data);
}
}
也就是说#
现在是当作一个运算符(operator )在用,而不是一个前缀(prefix)。
但为什么是#
?
答案是:老实说,我也想不到更好的了。如果你能找一个大家都满意的,我接受。
NOTE:
一个备选的运算符可以是
->
,但老实说我认为比#
更差劲。
7.其它
-
在本方案中,是默认用“对象或类”的自有成员列表来实现的,这意味着总是需要用类似
this#xxx
的语法来存取它。不过这并非唯一的方案。 -
关于类似于”采用词法上下文“来实现私有成员的问题,我另写文章来讨论吧。