摘要:現(xiàn)在使用創(chuàng)建類的私有成員,在語言層面上對該成員進(jìn)行了隱藏。應(yīng)該允許子類聲明成員,即使父類有一個同名的私有成員。其他支持私有成員的語言通常是允許的。假設(shè),公共成員和私有成員沖突,而是的私有成員,這時候外部存在。
譯者按:社區(qū)一直以來有一個聲音,就是反對使用 # 聲明私有成員。但是很多質(zhì)疑的聲音過于淺薄、人云亦云。其實 TC39 早就對此類呼聲做過回應(yīng),并且歸納了一篇 FAQ。翻譯這篇文章的同時,我會進(jìn)行一定的擴(kuò)展(有些問題的描述不夠清晰),目的是讓大家取得一定的共識。我認(rèn)為,只有你知其然,且知其所以然,你的質(zhì)疑才是有力量的。# 是怎么回事?譯者按:首先要明確的一點是,委員會對于私有成員很多設(shè)計上的抉擇是基于 ES 不存在類型檢查,為此做了很多權(quán)衡和讓步。這篇文章在很多地方也會提及這個不同的基本面。
# 是 _ 的替代方案。
class A { _hidden = 0; m() { return this._hidden; } }
之前大家習(xí)慣使用 _ 創(chuàng)建類的私有成員,但這僅僅是社區(qū)共識,實際上這個成員是暴露的。
class B { #hidden = 0; m() { return this.#hidden; } }
現(xiàn)在使用 # 創(chuàng)建類的私有成員,在語言層面上對該成員進(jìn)行了隱藏。
由于兼容性問題,我們不能去改變 _ 的工作機(jī)制。
譯者按:如果將私有成員的語義賦予 _,之前使用 _ 聲明公共成員的代碼就出問題了;而且就算你之前使用 _ 是用來聲明私有成員的,你能保證你心中的語義和現(xiàn)階段的語義完全一致么?所以為了慎重起見,將之前的一種錯誤語法(之前類成員以 # 開頭會報語法錯誤,這樣保證了以前不存在這樣的代碼)加以利用,變成私有成員語法。為什么不能通過 this.x 訪問?
譯者按:這個問題的意思是,如果類 A 有私有成員 #x(其中 # 是聲明私有,x 才是成員名),為什么內(nèi)部不能通過 this.x 訪問該成員,而一定要寫成 this.#x?譯者按:以下是一系列問題,問題 -> 解答 -> 延伸問題 -> 解答 ...
有 x 這個私有成員,不意味著不能有 x 這個公共成員,因此訪問私有成員不能是一個普通的查找。
這是 JS 的一個問題,因為它缺少靜態(tài)類型。靜態(tài)類型語言使用類型聲明區(qū)分外部公共/內(nèi)部私有的情況,而不需要標(biāo)識符。但是動態(tài)類型語言沒有足夠的靜態(tài)信息區(qū)分這些情況。
延伸問題 1:那么為什么這個提案允許一個類同時存在私有成員 #x 和公共成員 x ?如果私有成員和公共成員沖突,會破壞其“封裝性”。
私有成員很重要的一點是子類不需要知道它們。應(yīng)該允許子類聲明成員 x,即使父類有一個同名的私有成員。
譯者按:感覺第二點有點文不對題。
其他支持私有成員的語言通常是允許的。如下是完全合法的 Java 代碼:
class Base { private int x = 0; } class Derived extends Base { public int x = 0; }
譯者按:所謂的“封裝性”(encapsulation / hard private)是很重要的概念,最底下會有說明。最簡單的解釋是,外部不能以任意方式獲取私有成員的任何信息。假設(shè),公共成員和私有成員沖突,而 x 是 obj 的私有成員,這時候外部存在 obj.x。如果公私?jīng)_突,這里將會報錯,外部就嗅探到了 obj 存在 x 這個私有成員。這就違背了“封裝性”。延伸問題 2:為什么不使用運行時檢測,來決定訪問的是私有成員還是公共成員?
屬性訪問的語義已經(jīng)很復(fù)雜了,我們不想僅僅為了這個特性讓每次屬性訪問都更慢。
譯者按:屬性訪問的復(fù)雜性可以從 toFastProperties 和 toFastProperties 如何使對象的屬性更快 管窺一二
它(運行時檢測)還可能讓類的方法被非實例(比如普通對象)欺騙,使其在非實例的字段上進(jìn)行操作,從而造成私有成員的泄漏。這條評論 是一個例子。
延伸問題 3:當(dāng)類中聲明了私有成員 x 時,為什么不讓 obj.x 總是代表對私有成員的訪問?譯者按:如果不結(jié)合以上的例子,上面這句話其實很難理解。所以我覺得有必要擴(kuò)展一下,雖然有很多人認(rèn)為這個例子沒有說服力。
首先我希望你了解 Java,因為我會拿 Java 的代碼做對比。
其次我再明確一下,這個問題的根本在于 ES 沒有靜態(tài)類型檢測,而 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。
把這部分邏輯轉(zhuǎn)換為 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 的實例呢?現(xiàn)有的方案就是檢測在函數(shù)體中是否存在對入?yún)⒌乃接谐蓡T的訪問。比如上例中,函數(shù)中如果存在 a.#p,那么 a 就必須是 A 的實例。否則就會報 TypeError: attempted to get private field on non-instance
這就是為什么對私有成員的訪問必須在語法層面上體現(xiàn),而不能是簡單的運行時檢測。
譯者按:這個問題的意思是當(dāng)某個類聲明了私有成員 x,那么類中所有的成員表達(dá)式 sth.x 都表示是對 sth 的私有成員 x 的訪問。我覺得這是一個蠢問題,誰贊成?誰反對?
類方法經(jīng)常操作不是實例的對象。當(dāng) obj 不是實例的時候,如果 obj.x 突然間不再指的是 obj 的公共字段 x,僅僅是因為在類的某個地方聲明了私有成員 x,那就太奇怪了。
延伸問題 4:為什么不賦予 this 關(guān)鍵字特殊的語義?譯者按:這個問題針對前一個答案,你說 obj.x 不能做這種簡單粗暴的處理,那么 this.x 可以咯?
this 已經(jīng)是 JS 混亂的原因之一了;我們不想讓它變的更糟。同時,這還存在一個嚴(yán)重的重構(gòu)風(fēng)險:如果 const thiz = this; thiz.x 和 this.x 存在不同的語義,將會帶來很大的困擾。
而且除了 this,傳入的實例的私有成員將無法訪問(比如延伸問題 2 的 js 示例中傳入的 a)。
延伸問題 5:為什么不禁止除 this 之外的對象對私有成員的訪問?舉個栗子,這樣一來甚至可以使用 x 替代 this.x 表示對私有屬性的訪問?譯者按:這個問題再做了一次延伸,上面提到傳入的實例的私有成員不能訪問,這個問題是:不能訪問就不能訪問唄,有什么關(guān)系?
這個提案的目的是允許同類實例之間私有屬性的互相訪問。另外,使用裸標(biāo)識符(即使用 x 代替 this.x)不是 JS 的常見做法(除了 with,而 with 的設(shè)計也通常被認(rèn)為是一個錯誤)。
譯者按:一系列延伸問題到此結(jié)束,這類問題弄懂了基本上就掌握私有成員的核心語義和設(shè)計原則了。為什么 this.#x 可以訪問私有屬性,而 this[#x]不行?
這會讓屬性訪問的語義更復(fù)雜。
動態(tài)訪問違背了 私有 的概念。舉個栗子:
class Dict extends null { #data = something_secret; add(key, value) { this[key] = value; } get(key) { return this[key]; } } new Dict().get("#data"); // 返回了私有屬性延伸問題 1:賦予 this.#x 和 this[#x] 不同的語義是否破壞了當(dāng)前語法的穩(wěn)定性?
不完全是,但這確實是個問題。不過從某個角度上來說,this.#x 在當(dāng)前的語法中是非法的,這已經(jīng)破壞了當(dāng)前語法的穩(wěn)定性。
另一方面,this.#x 和 this[#x] 之間的差異比你看到的還要大,這也是當(dāng)前提案的不足。
為什么不能是 this#x,把 . 去掉?這是可行的,但是如果我們再簡化為 #x 就會出問題。
譯者按:這個說法很簡單,我直接列在下面
栗子:
class X { #y z() { w() #y() // 會被解析為w()#y } }
泛言之,因為 this.# 的語義更為清晰,委員會基本都支持這種寫法。
譯者按:這也是被認(rèn)為沒有說服力的一個說辭,因為委員會把 this#x 極端化成了 #x,然后描述 #x 的不足,卻沒有直接給出 this#x 的不足。為什么不是 private x?
這種聲明方式是其他語言使用的(尤其是 Java),這意味著使用 this.x 訪問該私有成員。
假設(shè) obj 是類實例,在類外部使用 obj.x 表達(dá)式,JS 將會靜默地創(chuàng)建或訪問公共成員,而不是拋出一個錯誤,這將會是 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;例子當(dāng)中,對屬性的創(chuàng)建如果不拋錯,是否就會創(chuàng)建一個公共字段?
如果創(chuàng)建了公共字段,調(diào)用 a.say() 打印的是公共字段還是私有字段?是不是打印哪個都感覺不對?
可能你會說,那就拋錯好了?那這樣就是運行時檢測,這個問題在上面有過描述。
因為這個功能非常有用,舉個栗子:判斷 Point 是否相等的 equals 方法。
實際上,其他語言由于同樣的原因也是這樣設(shè)計的;舉個栗子,以下是合法的 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; } }Unicode 這么多符號,為什么恰恰是 # ?
沒人說 # 是最漂亮最直觀的符號,我們用的是排除法:
@ 是最初的選擇,但是被 decorators 占用了。委員會考慮過交換 decorators 和 private 的符號(因為它們都還在提案階段),但最終還是決定尊重社區(qū)的習(xí)慣。
_ 對現(xiàn)有的項目代碼存在兼容問題,因為之前一直允許 _ 作為成員變量名的開頭。
其他之前用于中綴運算符,而非前綴運算符的。假設(shè)是可以的,比如%, ^, &, ?。考慮到我們的語法有點獨特 —— x.%y 當(dāng)前是非法的,所以不存在二義性。但無論如何,簡寫會帶來問題。舉個栗子,以下代碼看上去像是將符號作為中綴運算福:
class Foo { %x; method() { calculate().my().value() %x.print() } }
如上,開發(fā)人員看上去像是希望調(diào)用 this.%x 上的 print 方法。但實際上,將會執(zhí)行取余的操作!
其他不屬于 ASCII 或者 IDStart 的 Unicode 字符也可以使用,但對于許多用戶來說,他們很難在普通的鍵盤上找到對應(yīng)的字符。
最后,唯一的選項是更長的符號序列,但比起單個字符似乎不太理想。
譯者按:委員會還是舉了省略分號時的例子,可是上面也說了,就算是 #,也同樣存在問題。為什么這個提案不允許外部通過一些機(jī)制用于反射/訪問私有成員(比如說測試的時候)?其他語言也是這樣的嗎?
這樣做會違反“封裝性”。其他語言允許并不是一個充分的理由,尤其是在某些語言(例如 C++)中,是通過直接修改內(nèi)存實現(xiàn)的,而且這也不是一個必需的功能。
你所謂的“封裝性”和“硬隱私”是什么意思?意味著私有成員是完全內(nèi)部的:沒有任何類外部的 JS 代碼可以探測和影響到它們的存在,它們的成員名,它們的值,除非類自己選擇暴露他們。(包括子類和父類之間也是完全封裝的)。
意味著反射方法們,比如說 getOwnPropertySymbols 也不能暴露私有成員。
意味著如果一個類有一個私有成員 x,在類外部實例化類對象 obj,這時候通過 obj.x 訪問的應(yīng)該是公共成員 x,而不是訪問私有成員或者拋出錯誤。注意這里的現(xiàn)象和 Java 并不一致,因為 Java 可以在編譯時進(jìn)行類型檢查并且禁止通過成員名動態(tài)訪問內(nèi)容,除非是反射接口。
為什么這個提案會將封裝性作為目的?庫的作者們發(fā)現(xiàn),庫的使用者們開始依賴任何接口的公開部分,而非文檔上的那部分(即希望使用者們關(guān)注的部分)。一般情況下,他們并不認(rèn)為他們可以隨意的破壞使用者的頁面和應(yīng)用,即使使用者沒有參照他們的建議作業(yè)。因此,他們希望有真正的私有化可以隱藏實現(xiàn)細(xì)節(jié)。
雖然使用實例閉包或者 WeakMaps 已經(jīng)可以模擬真實的封裝(如下),但是兩種方式和類結(jié)合都過于浪費,而且還涉及了內(nèi)存使用的語義,也許這很讓人驚訝。此外, 實例閉包的方式還禁止同類的實例間共享私有成員([如上]](#share)),而 WeakMaps 的方式還存在一個暴露私有數(shù)據(jù)的潛在風(fēng)險,并且運行效率更低。
隱藏但不封裝也可以通過使用 Symbol 作為屬性名實現(xiàn)(如下)。
當(dāng)前提案正在努力推進(jìn)硬隱私,使 decorators 或者其他機(jī)制提供給類一個可選的逃生通道。我們計劃在此階段收集反饋,以確定這是否是正確的語義。
查看這個 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
然而這里還是存在一個潛在的問題。假設(shè)我們在構(gòu)造時添加一個回調(diào)函數(shù):
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 到此結(jié)束,可能有的地方會比較晦澀,多看幾遍寫幾個 demo 基本就懂了。我覺得技術(shù)存在 看山是山 -> 看山不是山 -> 看山還是山 這樣一個漸進(jìn)的過程,翻譯這篇 FAQ 也并非為 # 辯護(hù),只是現(xiàn)在很多質(zhì)疑還停留在 看山是山 這樣一個階段。我希望這篇 FAQ 可以讓你 看山不是山,最后達(dá)到 看山還是山 的境界:問題還是存在問題,不過是站在更全面和系統(tǒng)的角度去思考問題。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/98860.html
摘要:集成測試完成后,由運維同學(xué)從發(fā)起一個到分支,此時會會運行單元測試,構(gòu)建鏡像,并發(fā)布到預(yù)發(fā)布環(huán)境測試人員在預(yù)發(fā)布環(huán)境下再次驗證功能,團(tuán)隊做上線前的其他準(zhǔn)備工作運維同學(xué)合并,將為本次發(fā)布的代碼及鏡像自動打上版本號并書寫,同時發(fā)布到生產(chǎn)環(huán)境。 云原生 (Cloud Native) 是伴隨的容器技術(shù)發(fā)展出現(xiàn)的的一個詞,最早出自 Pivotal 公司(即開發(fā)了 Spring 的公司)的一本技術(shù)小...
摘要:集成測試完成后,由運維同學(xué)從發(fā)起一個到分支,此時會會運行單元測試,構(gòu)建鏡像,并發(fā)布到預(yù)發(fā)布環(huán)境測試人員在預(yù)發(fā)布環(huán)境下再次驗證功能,團(tuán)隊做上線前的其他準(zhǔn)備工作運維同學(xué)合并,將為本次發(fā)布的代碼及鏡像自動打上版本號并書寫,同時發(fā)布到生產(chǎn)環(huán)境。 云原生 (Cloud Native) 是伴隨的容器技術(shù)發(fā)展出現(xiàn)的的一個詞,最早出自 Pivotal 公司(即開發(fā)了 Spring 的公司)的一本技術(shù)小...
摘要:靜態(tài)屬性靜態(tài)方法目前支持靜態(tài)方法表示,類屬性及靜態(tài)屬性目前作為提案還未正式成為標(biāo)準(zhǔn)。在中,抽象類不能用來實例化對象,主要做為其它派生類的基類使用。不同于接口,抽象類可以包含成員的實現(xiàn)細(xì)節(jié)。中也是這樣規(guī)定的抽象類不允許直接被實例化。 嘗試重寫 在此之前,通過《JavaScript => TypeScript 入門》已經(jīng)掌握了類型聲明的寫法。原以為憑著那一條無往不利的規(guī)則,就可以開開心心的...
摘要:閉包的學(xué)術(shù)定義先來參考下各大權(quán)威對閉包的學(xué)術(shù)定義百科閉包,又稱詞法閉包或函數(shù)閉包,是引用了自由變量的函數(shù)。所以,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。 前言 上一章講解了閉包的底層實現(xiàn)細(xì)節(jié),我想大家對閉包的概念應(yīng)該也有了個大概印象,但是真要用簡短的幾句話來說清楚,這還真不是件容易的事。這里我們就來總結(jié)提煉下閉包的概念,以應(yīng)付那些非專人士的心血來潮。 閉包的學(xué)術(shù)...
閱讀 2458·2021-09-28 09:36
閱讀 3597·2021-09-22 15:41
閱讀 4383·2021-09-04 16:45
閱讀 1955·2019-08-30 15:55
閱讀 2847·2019-08-30 13:49
閱讀 824·2019-08-29 16:34
閱讀 2369·2019-08-29 12:57
閱讀 1679·2019-08-26 18:42