ECMAScript 私有字段
一. ES 私有字段简介
在介绍 ECMAScript 私有字段前,我们先目睹一下它的 “芳容”:
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
是不是觉得很别扭,目前 TC39 委员会以及对此达成了一致意见,并且该提案已经进入了 Stage 3。那么为什么使用 #
符号,而不是其他符号呢?
TC39 委员会解释道,他们也是做了深思熟虑最终选择了 # 符号,而没有使用 private 关键字。其中还讨论了把 private 和 # 符号一起使用的方案。并且还打算预留了一个 @ 关键字作为 protected 属性 。
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
与常规属性(甚至使用 private
修饰符声明的属性)不同,私有字段要牢记以下规则:
-
私有字段以
#
字符开头,有时我们称之为私有名称; -
每个私有字段名称都唯一地限定于其包含的类;
-
不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
-
私有字段不能在包含的类之外访问,甚至不能被检测到。
说到这里使用 #
定义的私有字段与 private
修饰符定义字段有什么区别呢?现在我们先来看一个 private
的示例:
class Person {
constructor(private name: string){}
}
let person = new Person("Semlinker");
console.log(person.name);
在上面代码中,我们创建了一个 Person 类,该类中使用 private
修饰符定义了一个私有属性 name
,接着使用该类创建一个 person
对象,然后通过 person.name
来访问 person
对象的私有属性,这时 TypeScript 编译器会提示以下异常:
Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解决这个异常呢?当然你可以使用类型断言把 person 转为 any 类型:
console.log((person as any).name);
通过这种方式虽然解决了 TypeScript 编译器的异常提示,但是在运行时我们还是可以访问到 Person
类内部的私有属性,为什么会这样呢?我们来看一下编译生成的 ES5 代码,也许你就知道答案了:
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
return Person;
}());
var person = new Person("Semlinker");
console.log(person.name);
这时相信有些小伙伴会好奇,在 TypeScript 3.8 以上版本通过 #
号定义的私有字段编译后会生成什么代码:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
以上代码目标设置为 ES2015,会编译生成以下代码:
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return privateMap.get(receiver);
};
var _name;
class Person {
constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() {
console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
通过观察上述代码,使用 #
号定义的 ECMAScript 私有字段,会通过 WeakMap
对象来存储,同时编译器会生成 __classPrivateFieldSet
和 __classPrivateFieldGet
这两个方法用于设置值和获取值。介绍完单个类中私有字段的相关内容,下面我们来看一下私有字段在继承情况下的表现。
二. ES 私有字段继承
为了对比常规字段和私有字段的区别,我们先来看一下常规字段在继承中的表现:
class C {
foo = 10;
cHelper() {
return this.foo;
}
}
class D extends C {
foo = 20;
dHelper() {
return this.foo;
}
}
let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'
很明显不管是调用子类中定义的 cHelper()
方法还是父类中定义的 dHelper()
方法最终都是输出子类上的 foo
属性。接下来我们来看一下私有字段在继承中的表现:
class C {
#foo = 10;
cHelper() {
return this.#foo;
}
}
class D extends C {
#foo = 20;
dHelper() {
return this.#foo;
}
}
let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'
通过观察上述的结果,我们可以知道在 cHelper()
方法和 dHelper()
方法中的 this.#foo
指向了每个类中的不同字段。关于 ECMAScript 私有字段的其他内容,我们不再展开,感兴趣的读者可以自行阅读相关资料。