No prefix! operator is Ok!

本文是系列文章,包括:

  • "Field提案"是什么东东 - 在这里
  • 关于废止proposal-class-fields提案的建议 - 在这里
  • 私有属性的实现 - 在这里
  • No prefix! operator is Ok!(本文)
  • (未完待续)

本文是对一份tc39提案的讨论。原有提案:https://github.com/tc39/proposal-class-fields

所有在我对原有提案的修改中,核心不是“不用#字符”,而是将它从一个前缀字符,变成了一个操作符。这一方面是使“声明语法”与“表达式运算”分开,另一方面也让这些修改与ECMAScript规范保持在语法上的一致性。

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"而不是"#"?

在现有类、对象等声明语法中是使用限定词来指示成员性质的,例如staticget等。除了生成器函数之外,并不使用限定符号或“前缀(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的语法来存取它。不过这并非唯一的方案。

  • 关于类似于”采用词法上下文“来实现私有成员的问题,我另写文章来讨论吧。