譯者按:社區一直以來有一個聲音,就是反對使用#
聲明私有成員。可是不少質疑的聲音過於淺薄、人云亦云。其實 TC39 早就對此類呼聲作過迴應,而且概括了一篇 FAQ。翻譯這篇文章的同時,我會進行必定的擴展(有些問題的描述不夠清晰),目的是讓你們取得必定的共識。我認爲,只有你知其然,且知其因此然,你的質疑纔是有力量的。譯者按:首先要明確的一點是,委員會對於私有成員不少設計上的抉擇是基於 ES 不存在類型檢查,爲此作了不少權衡和讓步。這篇文章在不少地方也會說起這個不一樣的基本面。html
#
是怎麼回事?#
是 _
的替代方案。java
class A { _hidden = 0; m() { return this._hidden; } }
以前你們習慣使用 _
建立類的私有成員,但這僅僅是社區共識,實際上這個成員是暴露的。git
class B { #hidden = 0; m() { return this.#hidden; } }
如今使用 #
建立類的私有成員,在語言層面上對該成員進行了隱藏。github
因爲兼容性問題,咱們不能去改變 _
的工做機制。閉包
譯者按:若是將私有成員的語義賦予_
,以前使用_
聲明公共成員的代碼就出問題了;並且就算你以前使用_
是用來聲明私有成員的,你能保證你心中的語義和現階段的語義徹底一致麼?因此爲了慎重起見,將以前的一種錯誤語法(以前類成員以 # 開頭會報語法錯誤,這樣保證了之前不存在這樣的代碼)加以利用,變成私有成員語法。
this.x
訪問?譯者按:這個問題的意思是,若是類 A 有私有成員 #x( 其中 # 是聲明私有,x 纔是成員名),爲何內部不能經過 this.x 訪問該成員,而必定要寫成 this.#x?譯者按:如下是一系列問題,問題 -> 解答 -> 延伸問題 -> 解答 ...函數
有 x
這個私有成員,不意味着不能有 x
這個公共成員,所以訪問私有成員不能是一個普通的查找。測試
這是 JS 的一個問題,由於它缺乏靜態類型。靜態類型語言使用類型聲明區分外部公共/內部私有的狀況,而不須要標識符。可是動態類型語言沒有足夠的靜態信息區分這些狀況。this
x
,即便父類有一個同名的私有成員。譯者按:感受第二點有點文不對題。
其餘支持私有成員的語言一般是容許的。以下是徹底合法的 Java 代碼:翻譯
class Base { private int x = 0; } class Derived extends Base { public int x = 0; }
譯者按:所謂的「封裝性」(encapsulation / hard private)是很重要的概念, 最底下會有說明。最簡單的解釋是,外部不能以任意方式獲取私有成員的任何信息。假設,公共成員和私有成員衝突,而x
是obj
的私有成員,這時候外部存在obj.x
。若是公私衝突,這裏將會報錯,外部就嗅探到了obj
存在x
這個私有成員。這就違背了「封裝性」。
屬性訪問的語義已經很複雜了,咱們不想僅僅爲了這個特性讓每次屬性訪問都更慢。設計
譯者按:屬性訪問的複雜性能夠從 toFastProperties 和 toFastProperties 如何使對象的屬性更快 管窺一二
它(運行時檢測)還可能讓類的方法被非實例(好比普通對象)欺騙,使其在非實例的字段上進行操做,從而形成私有成員的泄漏。這條評論 是一個例子。
譯者按:若是不結合以上的例子,上面這句話其實很難理解。因此我以爲有必要擴展一下,雖然有不少人認爲這個例子沒有說服力。
首先我但願你瞭解 Java,由於我會拿 Java 的代碼作對比。
其次我再明確一下,這個問題的根本在於 ES 沒有靜態類型檢測,而 TS 就不存在此類煩惱。
public class Main { public static void main(String[] args){ A a1 = new A(1); A a2 = new A(2); a1.swap(a2); a1.say(); a2.say(); } } class A { private int p; A(int p) { this.p = p; } public void swap(A a) { int tmp = this.p; this.p = a.p; a.p = tmp; } public void say() { System.out.println(this.p); } }以上的例子是一段正常的 Java 代碼,它的邏輯很簡單:聲明類 A,A 存在一個公共方法,容許實例和另外一個實例交換私有成員 p。
把這部分邏輯轉換爲 JS 代碼,而且使用 private 聲明
class A { private p; constructor(p) { this.p = p } swap(a) { let tmp = a.p; a.p = this.p; this.p = tmp; } say() { console.log(this.p); } }乍一看是沒有問題的,但 swap 有一個陷阱:若是傳入的對象不是 A 的實例,或者說只是一個普通的對象,是否是就能夠把私有成員 p 偷出來了?
JS 是不能作類型檢查的,那咱們怎麼聲明傳入的 a 必須是 A 的實例呢?現有的方案就是檢測在函數體中是否存在對入參的私有成員的訪問。好比上例中,函數中若是存在 a.#p,那麼 a 就必須是 A 的實例。不然就會報
TypeError: attempted to get private field on non-instance
這就是爲何對私有成員的訪問必須在語法層面上體現,而不能是簡單的運行時檢測。
x
時,爲何不讓 obj.x
老是表明對私有成員的訪問?譯者按:這個問題的意思是當某個類聲明瞭私有成員x
,那麼類中全部的成員表達式sth.x
都表示是對sth
的私有成員x
的訪問。我以爲這是一個蠢問題,誰同意?誰反對?
類方法常常操做不是實例的對象。當 obj
不是實例的時候,若是 obj.x
忽然間再也不指的是 obj
的公共字段 x
,僅僅是由於在類的某個地方聲明瞭私有成員 x
,那就太奇怪了。
this
關鍵字特殊的語義?譯者按:這個問題針對前一個答案,你說obj.x
不能作這種簡單粗暴的處理,那麼this.x
能夠咯?
this
已是 JS 混亂的緣由之一了;咱們不想讓它變的更糟。同時,這還存在一個嚴重的重構風險:若是 const thiz = this; thiz.x
和 this.x
存在不一樣的語義,將會帶來很大的困擾。
並且除了 this
,傳入的實例的私有成員將沒法訪問(好比延伸問題 2 的 js 示例中傳入的 a)。
this
以外的對象對私有成員的訪問?舉個栗子,這樣一來甚至可使用 x
替代 this.x
表示對私有屬性的訪問?譯者按:這個問題再作了一次延伸,上面提到傳入的實例的私有成員不能訪問,這個問題是:不能訪問就不能訪問唄,有什麼關係?
這個提案的目的是容許同類實例之間私有屬性的互相訪問。另外,使用裸標識符(即便用 x
代替 this.x
)不是 JS 的常見作法(除了 with
,而 with
的設計也一般被認爲是一個錯誤)。
譯者按:一系列延伸問題到此結束,這類問題弄懂了基本上就掌握私有成員的核心語義和設計原則了。
this.#x
能夠訪問私有屬性,而 this[#x]
不行?私有
的概念。舉個栗子:class Dict extends null { #data = something_secret; add(key, value) { this[key] = value; } get(key) { return this[key]; } } new Dict().get("#data"); // 返回了私有屬性
this.#x
和 this[#x]
不一樣的語義是否破壞了當前語法的穩定性?不徹底是,但這確實是個問題。不過從某個角度上來講,this.#x
在當前的語法中是非法的,這已經破壞了當前語法的穩定性。
另外一方面,this.#x
和 this[#x]
之間的差別比你看到的還要大,這也是當前提案的不足。
this#x
,把 .
去掉?這是可行的,可是若是咱們再簡化爲 #x
就會出問題。
譯者按:這個說法很簡單,我直接列在下面
栗子:
class X { #y z() { w() #y() // 會被解析爲w()#y } }
泛言之,由於 this.#
的語義更爲清晰,委員會基本都支持這種寫法。
譯者按:這也是被認爲沒有說服力的一個說辭,由於委員會把this#x
極端化成了#x
,而後描述#x
的不足,卻沒有直接給出this#x
的不足。
private x
?這種聲明方式是其餘語言使用的(尤爲是 Java),這意味着使用 this.x
訪問該私有成員。
假設 obj
是類實例,在類外部使用 obj.x
表達式,JS 將會靜默地建立或訪問公共成員,而不是拋出一個錯誤,這將會是 bug 的主要潛在來源。
它還使聲明和訪問對稱,就像公共成員同樣:
class A { pub = 0; #priv = 1; m() { return this.pub + this.#priv; } }
譯者按:這裏說明了爲何使用
#
不使用private
的主要緣由。咱們理一下:若是咱們使用
private
class A { private p; say() { console.log(this.p); } } const a = new A; console.log(a.p); a.p = 1;例子當中,對屬性的建立若是不拋錯,是否就會建立一個公共字段?
若是建立了公共字段,調用a.say()
打印的是公共字段仍是私有字段?是否是打印哪一個都感受不對?
可能你會說,那就拋錯好了?那這樣就是運行時檢測,這個問題在上面有過描述。
由於這個功能很是有用,舉個栗子:判斷 Point
是否相等的 equals
方法。
實際上,其餘語言因爲一樣的緣由也是這樣設計的;舉個栗子,如下是合法的 Java 代碼
class Point { private int x = 0; private int y = 0; public boolean equals(Point p) { return this.x == p.x && this.y == p.y; } }
#
?沒人說 #
是最漂亮最直觀的符號,咱們用的是排除法:
@
是最初的選擇,可是被 decorators
佔用了。委員會考慮過交換 decorators
和 private
的符號(由於它們都還在提案階段),但最終仍是決定尊重社區的習慣。_
對現有的項目代碼存在兼容問題,由於以前一直容許 _
做爲成員變量名的開頭。%
, ^
, &
, ?
。考慮到咱們的語法有點獨特 —— x.%y
當前是非法的,因此不存在二義性。但不管如何,簡寫會帶來問題。舉個栗子,如下代碼看上去像是將符號做爲中綴運算福:class Foo { %x; method() { calculate().my().value() %x.print() } }
如上,開發人員看上去像是但願調用 this.%x
上的 print
方法。但實際上,將會執行取餘的操做!
最後,惟一的選項是更長的符號序列,但比起單個字符彷佛不太理想。
譯者按:委員會仍是舉了省略分號時的例子,但是上面也說了,就算是
#
,也一樣存在問題。
這樣作會違反「封裝性」。其餘語言容許並非一個充分的理由,尤爲是在某些語言(例如 C++)中,是經過直接修改內存實現的,並且這也不是一個必需的功能。
意味着私有成員是徹底內部的:沒有任何類外部的 JS 代碼能夠探測和影響到它們的存在,它們的成員名,它們的值,除非類本身選擇暴露他們。(包括子類和父類之間也是徹底封裝的)。
意味着反射方法們,好比說 getOwnPropertySymbols 也不能暴露私有成員。
意味着若是一個類有一個私有成員 x
,在類外部實例化類對象 obj
,這時候經過 obj.x
訪問的應該是公共成員 x
,而不是訪問私有成員或者拋出錯誤。注意這裏的現象和 Java 並不一致,由於 Java 能夠在編譯時進行類型檢查而且禁止經過成員名動態訪問內容,除非是反射接口。
WeakMaps
已經能夠模擬真實的封裝(以下),可是兩種方式和類結合都過於浪費,並且還涉及了內存使用的語義,也許這很讓人驚訝。此外, 實例閉包的方式還禁止同類的實例間共享私有成員([如上]](#share)),而 WeakMaps
的方式還存在一個暴露私有數據的潛在風險,而且運行效率更低。Symbol
做爲屬性名實現(以下)。當前提案正在努力推動硬隱私,使 decorators 或者其餘機制提供給類一個可選的逃生通道。咱們計劃在此階段收集反饋,以肯定這是不是正確的語義。
查看這個 issue 瞭解更多。
WeakMap
如何模擬封裝?const Person = (function() { const privates = new WeakMap(); let ids = 0; return class Person { constructor(name) { this.name = name; privates.set(this, { id: ids++ }); } equals(otherPerson) { return privates.get(this).id === privates.get(otherPerson).id; } }; })(); let alice = new Person("Alice"); let bob = new Person("Bob"); alice.equals(bob); // false
然而這裏仍是存在一個潛在的問題。假設咱們在構造時添加一個回調函數:
const Person = (function() { const privates = new WeakMap(); let ids = 0; return class Person { constructor(name, makeGreeting) { this.name = name; privates.set(this, { id: ids++, makeGreeting }); } equals(otherPerson) { return privates.get(this).id === privates.get(otherPerson).id; } greet(otherPerson) { return privates.get(this).makeGreeting(otherPerson.name); } }; })(); let alice = new Person("Alice", name => `Hello, ${name}!`); let bob = new Person("Bob", name => `Hi, ${name}.`); alice.equals(bob); // false alice.greet(bob); // === 'Hello, Bob!'
乍看好像沒有問題,可是:
let mallory = new Person("Mallory", function(name) { this.id = 0; return `o/ ${name}`; }); mallory.greet(bob); // === 'o/ Bob' mallory.equals(alice); // true. 錯了!
Symbols
提供隱藏但不封裝的屬性?const Person = (function() { const _id = Symbol("id"); let ids = 0; return class Person { constructor(name) { this.name = name; this[_id] = ids++; } equals(otherPerson) { return this[_id] === otherPerson[_id]; } }; })(); let alice = new Person("Alice"); let bob = new Person("Bob"); alice.equals(bob); // false alice[Object.getOwnPropertySymbols(alice)[0]]; // == 0,alice 的 id.
譯者按:FAQ 到此結束,可能有的地方會比較晦澀,多看幾遍寫幾個 demo 基本就懂了。我以爲技術存在看山是山 -> 看山不是山 -> 看山仍是山
這樣一個漸進的過程,翻譯這篇 FAQ 也並不是爲#
辯護,只是如今不少質疑還停留在看山是山
這樣一個階段。我但願這篇 FAQ 可讓你看山不是山
,最後達到看山仍是山
的境界:問題仍是存在問題,不過是站在更全面和系統的角度去思考問題。