JavaScript 和 TypeScript 的封裝性 —— 私有成員

JavaScript 使用了基於原型模式的 OOP 實現,一直以來,其封裝性都不太友好。爲此,TypeScript 在對 JavaScript 類型進行加強的同時,特別關注了「類」定義。TS 的類定義起來更接近於 Java 和 C# 的語法,還容許使用 privateprotectedpublic 訪問修飾符聲明成員訪問限制,並在編譯期進行檢查。前端

顯然 ECMAScript 受到啓發,在 ES2015 中引入了新的類定義語法,並開始思考成員訪問限制的問題,提出了基於 Symbol 和閉包私有成員定義方案,固然這個方案使用起來並不太能被接受。又通過長達 4 年思考、設計和討論,最終在 ES2019 中發佈了使用 # 號來定義私有成員的規範。Chrome 74+ 和 Node 12+ 已經實現了該私有成員定義的規範。typescript

JavaScript 和 ECMAScript 有什麼關係?

ECMAScript 由 ECMA-262 標準定義,是一個語言規範(Specification);JavaScript 是該規範的一個實現。拿上面的問題去搜索引擎上搜索一下,能夠查閱到更詳盡的答案。json

1. ES 規範中的私有成員定義

1.1. 正確示例

先來看一個示例: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

1.2. 錯誤調用示例

私有成員的訪問限制決定了,這個成員能夠在定義它的類的內部訪問,無論它是靜態 (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);
})();

1.3. 私有方法

雖然 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
})();

2. TypeScript 中的私有成員

都已經 2020 了,講到 JavaScript 而不提 TypeScript 有點說不過去。可是若是你確實一點不會 TypeScript,也暫時不想去了解它,這部分能夠跳過。

🖊 做者「邊城」會在近期推出與 TypeScript 有關的視頻教程,即便難免費,也會很是超值。請關注「邊城客棧」訂閱號跟蹤此視頻教程的最新消息。

2.1. 訪問限定修飾符

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.nametest.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 中具備存在的意義。

2.2. TypeScript 和 #privateField

上面提到,若是使用 TypeScript 寫一個庫,使用 privateprotected 來限定成員訪問,在其用戶一樣使用 TypeScript 的時候不會有問題。但其用戶使用 JavaScript 的時候,卻並不能受到指望的限制。所以 TypeScript 引入 #privateField 是意義的。

不過 TypeScript 並無直接把 private 修飾符和 #privateField 關聯起來,它在 v3.8 的發行公告 中解釋了兩者的主要區別在於運行時訪問權限。

在 TypeScript 中使用 #privateField,從語法檢查上來講和 private 區別不大,都限制爲僅在內部可訪問,因此在聲明 #privateField 的時候,不容許添加訪問限制修飾符:

  • 若是添加 publicprotected,語義相悖
  • 若是添加 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) 這句話的轉譯結果。

  • 若是配置爲 ESNEXT,轉譯結果不變,仍然是 test.#name。因爲外部不可訪問私有成員,這樣調用會引發語法錯誤;
  • 若是配置爲 ES2020 或之前版本,轉譯結果會直接丟掉對私有字段的訪問:console.log(test.);,直接引起的語法錯誤。

private 和 #privateField 的選擇問題上,我我的建議現階段(現階段 TS 的最高穩定 Target 版本是 TS2020)仍然使用 private。TS 會把 #privateField 轉義成閉包環境下的 privateMap,雖然實現了功能,但看起來彆扭。固然若是你不在乎這個問題,或者使用 ESNext 做爲 Target,那不妨早一點嘗試新的語法。

3. 其餘私有成員解決方案

3.1. 使用閉包環境下的 Symbol

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 仍是很方便的。

3.2. 用隨機屬性名代替 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 的值會隨機生成,因此用戶並不知道它究竟是什麼,也就不能經過它來訪問成員,以此達到私有化的目的。

3.3. 擡個槓

不論是 Symbol 仍是隨機屬性名實現的私有成員,都有漏洞可鑽,因此防君子不防小人。提示一下,細節就不說了:

  • Object.getOwnPropertySymbols()
  • Object.getOwnPropertyNames()

邊城客棧

請關注公衆號邊城客棧

看完了先別走,點個贊 ⇓ 啊,讚揚 ⇘ 就更好啦!

相關文章
相關標籤/搜索