1.类的自定义属性
要定义在单击时递增的计数器窗口组件,可以使用ES2015定义以下内容:
class Counter extends HTMLElement {
clicked() {
this.x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
this.x = 0;
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.x.toString();
}
}
window.customElements.define('num-counter', Counter);
复制代码
2. 预先声明属性
使用ESnext字段声明提议,上面的示例可以写成:
class Counter extends HTMLElement {
x = 0;
clicked() {
this.x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.x.toString();
}
}
window.customElements.define('num-counter', Counter);
复制代码
在上面的示例中,您可以看到使用语法x = 0声明的字段。您还可以将没有初始值设定项的字段声明为x。
- 通过预先声明字段,类定义变得更加自我文档化;
- 实例经历较少的状态转换,因为声明的字段始终存在。
私有属性
下面的示例有一些实现细节,属性可以更好地保留在内部。 使用ESnext私有字段和方法,可以将定义细化为:
class Counter extends HTMLElement {
#x = 0;
clicked() {
this.#x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.#x.toString();
}
}
window.customElements.define('num-counter', Counter);
复制代码
要使字段私有,只需给它们一个以#开头的名称。
通过定义在类外部不可见的内容,ESnext提供了更强大的封装,确保您的类的用户不会因为依赖于内部而产生意外。
请注意,ESnext仅在字段声明中提供预先声明的私有字段,正常属性的方式不能创建。
主要设计要点
使用Object.defineProperty创建的公有属性
公有属性声明使用Object.defineProperty(我们在TC39术语中将[[Define]]语义引用)定义实例上的字段,而不是使用this.field = value; (称为[[Set]]语义)。 以下是影响的示例:
class A {
set x(value) { console.log(value); }
}
class B extends A {
x = 1;
}
复制代码
使用所采用的语义,new B()将导致一个对象具有值为1的属性x,并且不会将任何内容写入控制台。 使用备用[[Set]]语义,1将被写入控制台,并且尝试访问该属性将导致TypeError(因为缺少getter)。
在[[Set]]和[[Define]]之间进行选择是一种设计决策,它对比了不同类型的行为预期:预期字段将作为数据属性创建,而不管超类包含什么;预期setter将被调用。经过长时间的讨论,TC39确定了[[Define]]语义,发现保持第一个期望很重要。
基于Object.defineProperty的公共字段语义的决定是基于TC39内部的广泛讨论和与开发人员社区的协商。不幸的是,社区相当分裂,而TC39则强烈支持Object.defineProperty。
作为一种缓解,decorators建议提供了编写decorator的工具,使公共字段声明使用[[Set]]语义。即使您不同意默认值,也可以使用其他选项。(无论TC39选择哪种缺省值,都是这种情况。)
公共字段在Chrome 72中带有[[Define]]语义,这个语义决定不太可能被重新访问。
没有初始化项的字段被设置为undefined
无论是否存在初始化,公共字段声明和私有字段声明都会在实例中创建一个字段。如果没有初始化器,则将字段设置为undefined。这与某些转置器实现稍有不同,后者将完全忽略没有初始化器的字段声明。
例如,在下面的示例中,new D将生成一个对象,其y属性未定义,而不是1。
class C {
y = 1;
}
class D extends C {
y;
}
复制代码
将没有初始化项的字段设置为undefined(而不是擦除它们)的语义是,字段声明提供了可靠的基础,以确保在创建的对象上呈现属性。这有助于程序员将对象保持在相同的一般状态,这可以很容易地进行推理,有时在实现中更易于优化。
私有语法
私有字段基于使用#的语法,在声明字段和访问字段时都使用#。
class X {
#foo;
method() {
console.log(this.#foo)
}
}
复制代码
这种语法试图既简洁又直观,尽管它与其他编程语言有很大的不同。 没有私有的计算属性名:#foo是一个私有标识符,#[foo]是一个语法错误。
没有后门访问私有属性
私有字段提供了一个强大的封装边界:从类外部访问私有字段是不可能的,除非有一些显式代码来公开它(例如,提供getter)。这与JavaScript属性不同,JavaScript属性支持各种反射和元编程,而类似于闭包和WeakMap等机制,这些机制不提供对其内部的访问。
执行初始化
在构造函数运行时,公共字段和私有字段都按声明的顺序添加到实例中。初始化器将为每个类实例重新计算。字段在初始化器运行后立即添加到实例中,然后再计算下面的初始化器。
作用域:作为初始化器表达式中的这个值,正在构造的实例位于作用域中。新的。目标未定义,如在方法中。对参数的引用是早期的错误。超方法调用Super .method()在初始化器中可用,但是超构造函数调用Super()是一个语法错误。即使类在异步函数/genenerator中声明,初始化器中也不能使用wait和yield。
当计算字段初始化器并将字段添加到实例时:
- 基类:在构造函数执行的开始,甚至在参数析构之前。
- 派生类:就在super()返回之后。(调用super()的灵活性导致许多实现为这种情况创建了一个单独的不可见initialize()方法。)
如果派生类中没有调用super(),并且没有将其他一些公共和私有字段添加到实例中,也没有计算初始化器。对于基类,初始化器总是被求值,即使构造函数最终返回其他东西。new.initialize proposal将添加一种方法,以编程方式向一个实例添加字段,而该实例不是来自基类中的super()/这个值。