JavaScript 使用了基于原型模式的 OOP 实现,一直以来,其封装性都不太友好。为此,TypeScript 在对 JavaScript 类型进行加强的同时,特别关注了“类”定义。TS 的类定义起来更接近于 Java 和 C# 的语法,还容许使用 private
、protected
和 public
访问修饰符声明成员访问限制,并在编译期进行检查。前端
显然 ECMAScript 受到启发,在 ES2015 中引入了新的类定义语法,并开始思考成员访问限制的问题,提出了基于 Symbol 和闭包私有成员定义方案,固然这个方案使用起来并不太能被接受。又通过长达 4 年思考、设计和讨论,最终在 ES2019 中发布了使用 #
号来定义私有成员的规范。Chrome 74+ 和 Node 12+ 已经实现了该私有成员定义的规范。typescript
JavaScript 和 ECMAScript 有什么关系?ECMAScript 由 ECMA-262 标准定义,是一个语言规范(Specification);JavaScript 是该规范的一个实现。拿上面的问题去搜索引擎上搜索一下,能够查阅到更详尽的答案。json
先来看一个示例:segmentfault
class Test { static #greeting = "Hello"; #name = "James"; test() { console.log(`${Test.#greeting} ${this.#name}`); } } // 用一个 IIFE 来代替定义并执行 main() (() => { const t = new Test(); t.test(); // OUTPUT: Hello James })();
这个示例在 Chrome 74+ 和最新版的 Edge 等浏览览器的开发者工具控制台中运行都没有问题。闭包
📝 <u>小技巧</u>试验代码时每每须要在开发者工具控制台中屡次粘贴相似的代码,像
const t = ...
这样的代码在第二次运行的时候会报 “Identifier 't' has already been declared”这样的错误。框架为了不这种错误,能够将须要直接运行的代码封装在 IIFE 中,即
(() => { ... })()
。dom同理,在不支持顶层
await
的环境中,也能够用(async () => { ... })()
来封装须要直接执行的异步代码。ecmascript
私有成员的访问限制决定了,这个成员能够在定义它的类的内部访问,无论它是静态 (static) 成员仍是实例成员。稍稍改一下代码能够很容易验证这一点:异步
// 前端的类定义不变,只改一下 IIFE 中的测试代码 (() => { // SyntaxError: Private field '#greeting' must be declared in an enclosing class console.log(Test.#greeting); // SyntaxError: Private field '#name' must be declared in an enclosing class console.log(new Test().#name); })();
虽然 MDN 上一直描述的是私有字段 (private fields),但它给的语法中包含了私有方法的定义async
来自 MDN: Private class fields 的 Syntax 部分:
class ClassWithPrivateMethod { #privateMethod() { return 'hello world' } }(这部分代码风格和其余代码风格不一样,它是原样从 MDN 抄下来的,非“边城”风格)
很不幸,即便在最新的 Chrome 83 中尝试上面的代码,也只能获得语法错误。Nodejs 和 Edge 都是基于 Chrome 的,因此会获得相同的结果。而 Firefox 压根儿不支持私有成员特性。
不过 JS 很灵活,有很是神奇的 this
指向规则。咱们能够用定义字段的方式来定义方法:
class Test { #name; constructor(name) { this.#name = name; } #greet = () => { console.log(`hello ${this.#name}`); } test() { this.#greet(); } } (() => { new Test("James").test(); // OUTPUT: hello James })();
都已经 2020 了,讲到 JavaScript 而不提 TypeScript 有点说不过去。可是若是你确实一点不会 TypeScript,也暂时不想去了解它,这部分能够跳过。🖊 做者“边城”会在近期推出与 TypeScript 有关的视频教程,即便难免费,也会很是超值。请关注“边城客栈”订阅号跟踪此视频教程的最新消息。
TypeScript 发明之初就提供了私有成员解决方案,跟 Java 和 C# 相似,经过添加访问限定修饰符来声明成员的可访问级别:
public
,公共可访问,不加修饰符默认此级别;protected
,子类可访问;private
,仅内部可访问仍是拿实例来讲话:
class Test { private name: string; constructor(name: string) { this.name = name; } private greet() { console.log(`hello ${this.name}`); } test() { this.greet(); } } (() => { const test = new Test("James"); console.log(test.name); test.greet(); test.test(); })();
这段代码能够拿到 TypeScript Playground 去运行,打开控制台来查看结果。不过我更推荐使用 Playgroud v3 beta ,从 Playground 页面右上角的“Try Playground v3 beta”可进入。
在 JS 区,咱们能够看到转义后的 Test 类定义,已经去掉了访问限定修饰符:
class Test { constructor(name) { this.name = name; } greet() { console.log(`hello ${this.name}`); } test() { this.greet(); } }
这就意味着,下面的测试代码在 JS 环境中彻底能够正确执行,不会受限。在控制台,或者 Playground v3 的 Logs 部分,能够看到正常的输出
[LOG]: James [LOG]: hello James [LOG]: hello James
不过在编辑器内,咱们能够看到 test.name
和 test.greet()
被标记为有错。鼠标移上去能够看到具体的错误信息。这些错误信息在 Playground v3 的 Errors 部分也能够看到:
Property 'name' is private and only accessible within class 'Test'. Property 'greet' is private and only accessible within class 'Test'.
TypeScript 扩展了更为严格的语法,并借助 LSP 和编译器来帮助开发者在开发环境中尽早发现并解决存在或替在的问题。这就是 TS 为开发者带来的最大好处,也是 TS 发展如此迅速的缘由之一。然而,正如上面的示例所示,TS 编译出来的 JS 库并不能限制最终用户如何使用。因此即便 TS 有了 private
,#privateField
在仍然在 TS 中具备存在的意义。
上面提到,若是使用 TypeScript 写一个库,使用 private
或 protected
来限定成员访问,在其用户一样使用 TypeScript 的时候不会有问题。但其用户使用 JavaScript 的时候,却并不能受到指望的限制。所以 TypeScript 引入 #privateField 是意义的。
不过 TypeScript 并无直接把 private
修饰符和 #privateField
关联起来,它在 v3.8 的发行公告 中解释了两者的主要区别在于运行时访问权限。
在 TypeScript 中使用 #privateField,从语法检查上来讲和 private
区别不大,都限制为仅在内部可访问,因此在声明 #privateField 的时候,不容许添加访问限制修饰符:
public
或 protected
,语义相悖private
,没有必要上面的示例,若是把 private name
改成 #name
,咱们不只会获得编译期错误,还会获得运行时错误:
[ERR]: Private field '#name' must be declared in an enclosing class
或者
[ERR]: Unexpected token ')'
获得哪一个错误取决于 tsconfig.json
中的 target
配置,它决定了 console.log(test.#name)
这句话的转译结果。
test.#name
。因为外部不可访问私有成员,这样调用会引发语法错误;console.log(test.);
,直接引起的语法错误。在 private
和 #privateField 的选择问题上,我我的建议现阶段(现阶段 TS 的最高稳定 Target 版本是 TS2020)仍然使用 private
。TS 会把 #privateField 转义成闭包环境下的 privateMap
,虽然实现了功能,但看起来别扭。固然若是你不在乎这个问题,或者使用 ESNext 做为 Target,那不妨早一点尝试新的语法。
ES2015 引入了 Symbol 这一特殊的数据类型。说它特殊,由于它能够作到每次产生的 Symbol 毫不相同,好比
const a = Symbol("key"); const b = Symbol("key"); console.log(a === b); // false
此外,Symbol 能够做为对象的 key 使用:
const o = {}; const key = Symbol("key"); o[key] = "something"; console.log(o[key]); // OUTPUT: something
若是在闭包环境下使用 Symbol,让外界拿不到这个 Symbol,就能够实现私有属性。下面是使用 JS 写的示例,TS 相似:
// @file test.mjs const NAME = Symbol("name"); export class Test { constructor(name) { this[NAME] = name; } test() { console.log(`hello ${this[NAME]}`); } }
// @file index.mjs import { Test } from "./test.mjs"; const t = new Test("James"); // OUTPUT: hello James t.test(); // OUTPUT: undefined console.log(t[Symbol("name")]);
模块 —— 不论是 ESM 仍是 CommonJS Module —— 都是闭包环境。因此在模块化框架中使用 Symbol 仍是很方便的。
对于没有 Symbol 的环境,可使用随机属性名代替。不过既然是不支持 Symbol 的环境,显然也不支持 class, let/const, ESM 等特性,因此示例代码看起来比较古老:
var Test = (function () { const NAME = ("name__" + Math.random()); function Test(name) { this[NAME] = name; } Test.prototype.test = function () { console.log("hello " + this[NAME]); }; return Test; })(); var t = new Test("James"); t.test();
因为每次运行时建立 Test 构造函数的时候,NAME
的值会随机生成,因此用户并不知道它究竟是什么,也就不能经过它来访问成员,以此达到私有化的目的。
不论是 Symbol 仍是随机属性名实现的私有成员,都有漏洞可钻,因此防君子不防小人。提示一下,细节就不说了:
Object.getOwnPropertySymbols()
Object.getOwnPropertyNames()
请关注公众号边城客栈⇗
看完了先别走,点个赞 ⇓ 啊,赞扬 ⇘ 就更好啦!