翻譯,原始文章:「Class-fields-proposal」 or 「what went wrong in tc39 committee」javascript
一直以來,咱們都指望有一天能在JavaScript中較爲簡單地使用其餘語言常見的封裝語法。好比,咱們想要類屬性/字段的語法,而且它的實現方式並不會破壞現有的程序。如今看起來,這一天已經到來:在TC39委員會的努力之下,類字段提案已經進入stage 3
,甚至已經被Chrome實現html
老實說,我很樂意寫一篇文章,描述爲何您必須使用這個新功能以及如何實現它。但惋惜我沒法這麼作。vue
參考文檔在此不贅述了,具體參考:原始說明,FAQ,規範變動。java
類字段說明和用法:git
class A {
x = 1;
method() {
console.log(this.x);
}
}
複製代碼
從外部代碼訪問字段:github
const a = new A();
console.log(a.x);
複製代碼
一眼看去稀鬆日常,有些人可能會說咱們在Babel和TypeScript中這樣使用多年了。chrome
但有一件事值得注意:這個語法使用[[Define]]
語義而不是咱們習慣的[[Set]]
語義。這意味着實際上上面的代碼不等價於如下用法:typescript
class A {
constructor() {
this.x = 1;
}
method() {
console.log(this.x);
}
}
複製代碼
而等價於下述用法:npm
class A {
constructor() {
Object.defineProperty(this, "x", {
configurable: true,
enumerable: true,
writable: true,
value: 1
});
}
method() {
console.log(this.x);
}
}
複製代碼
儘管在這個例子下,兩種用法實際表現幾乎沒有什麼區別,但實際有一個很重要的區別。咱們假設咱們有一個像這樣的父類:編程
class A {
x = 1;
method() {
console.log(this.x);
}
}
複製代碼
從該父類派生出一個子類以下:
class B extends A {
x = 2;
}
複製代碼
而後使用:
const b = new B();
b.method(); // prints 2 to the console
複製代碼
而後爲了某些(不重要的)緣由,咱們以一種彷佛向後兼容的方式改變了A類:
class A {
_x = 1; // for simplicity let's skip that public interface got new property here
get x() { return this._x; };
set x(val) { return this._x = val; };
method() {
console.log(this._x);
}
}
複製代碼
對於[[Set]]
語義,它確實是向後兼容的。 可是對於[[Define]]
不是。 如今調用b.method()
會將打印1
而不是2
到控制檯。緣由是在Object.defineProperty
的做用下,不會調用A
類聲明的屬性描述符以及其getter/setter。 所以,在派生類中,咱們以相似變量詞法做用域的方式隱藏了父類x
性:
const x = 1;
{
const x = 2;
}
複製代碼
咱們可使用no-shadowed-variable
/no-shadow
這樣的liner規則幫助咱們檢測常見的詞法做用域變量隱藏。 可是不幸的是,不太可能有人會建立no-shadowed-class-field
這樣的規則幫助咱們規避類字段的隱藏。
儘管如此,我並非[[Define]]
語義的的堅決反對者(儘管我更喜歡[[Set]]
語義),由於它有它的好的優勢。然而,它的優勢並無超過主要的缺點——咱們多年來一直使用[[Set]]
語義,由於它是babel6
和TypeScript
的默認行爲。
我不得不強調一下,
babel7
改變了默認行爲。
咱們來看看這個提案中最具爭議的部分。 它是如此有爭議:
stage 3
;聲明私有字段的語法:
class A {
#priv;
}
複製代碼
並使用如下表示法訪問:
class A {
#priv = 1;
method() {
console.log(this.#priv);
}
}
複製代碼
這個語法看起來違反直覺,而且很不直觀(this.#priv != this['#priv']
),而且沒有使用JavaScript的保留字privaye
/protected
(這可能會讓已經使用TypeScript的開發者感到傷腦筋),而且爲更多的訪問級別的設計留下隱患。在這樣的情境下,我深刻的調查並參與了相關討論。
若是這僅僅與語法形式有關,即主觀審美上咱們難以接受,那麼最後咱們或許能夠忍受這樣的語法並習慣它。 可是,還有一個語義問題……
讓咱們來看看現有提案背後的語義。 咱們可以在沒有新語法可是保持原有行爲的狀況下重寫前面的示例:
const privatesForA = new WeakMap();
class A {
constructor() {
privatesForA.set(this, {});
privatesForA.get(this).priv = 1;
}
method() {
console.log(privatesForA.get(this).priv);
}
}
複製代碼
順便說一句,一名委員會成員使用這種語義建立了一個小型實用程序庫,這使咱們如今就可使用私有狀態。 他的目標是代表這種功能被委員會高估了。其格式化代碼只有27行。
很棒,咱們能夠擁有硬私有
了,沒法從外部代碼訪問/攔截/跟蹤內部的字段,同時咱們甚至能夠經過如下方式訪問同一類的另外一個實例的私有:
isEquals(obj) {
return privatesForA.get(this).id === privatesForA.get(obj).id;
}
複製代碼
這一切都很是方便,除了這個語義不只包括封裝
,還包括brand-checking
(您沒必要谷歌這個術語,由於您不太可能找到任何相關的信息)。 brand-checking
與鴨子類型
相反,從某種意義上說,它根據特定代碼肯定特定對象而非根據該對象的公共接口肯定對象。 實際上,這種檢查有其本身的用途——在大多數狀況下,它們與在同一進程中安全執行不受信任的代碼有關,能夠直接共享對象而無需序列化/反序列化開銷。
可是一些工程師堅持認爲這是正確封裝的要求。
儘管這是一個很是有趣的可能實現,它涉及膜
模式(簡短和詳盡描述),Realms
提案和Mark Samuel Miller的計算機科學研究(他也是委員會成員),可是根據個人經驗,它彷佛並不常見於大多數開發人員的平常工做中。
brand-checking
的問題正如我以前所說,brand-checking
與鴨子類型相反。 在實踐中,這意味着使用如下代碼:
const brands = new WeakMap();
class A {
constructor() {
brands.set(this, {});
}
method() {
return 1;
}
brandCheckedMethod() {
if (!brands.has(this)) throw 'Brand-check failed';
console.log(this.method());
}
}
複製代碼
brandCheckedMethod
只能被A
的實例調用,即便target符合此類的全部結構,此方法也會拋出異常:
const duckTypedObj = {
method: A.prototype.method.bind(duckTypedObj),
brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj),
};
duckTypedObj.method(); // no exception here and method returns 1
duckTypedObj.brandCheckedMethod(); // throws an exception
複製代碼
顯然,這個例子是刻意設計的,而且這種鴨子類型
的好處是值得懷疑的。除非咱們談論Proxy
。 代理有一個很是重要的使用場景——元編程。 爲了使代理執行全部必需的有用工做,代理包裝的對象的方法應該在代理的上下文中調用,而不是在目標的中調用:
const a = new A();
const proxy = new Proxy(a, {
get(target, p, receiver) {
const property = Reflect.get(target, p, receiver);
doSomethingUseful('get', retval, target, p, receiver);
return (typeof property === 'function')
? property.bind(proxy) // actually bind here is redundant, but I want to focus your attention on method's context
: property;
}
});
複製代碼
調用proxy.method(); 將致使作一些在代理中聲明並返回1
的有用工做,當調用proxy.brandCheckedMethod();
而不是作一些有用的工做兩次將致使拋出異常,由於a !== proxy
而且brand-check
失敗了。
固然,咱們能夠在真實目標而不是代理的上下文中執行方法/函數,而且在某些狀況下它就足夠了(例如實現膜
模式),但它並不是對於全部狀況都是夠用的(例如,實現反應式屬性:MobX 5已經使用代理實現,Vue.js和Aurelia正在試驗這種方法以便用於將來版本)。
一般,雖然brand-check
須要顯式聲明,但這並非問題:開發人員只需選擇他/她須要哪一種權衡以及緣由。 在明確的brand-check
的狀況下,它能夠以容許其與某些可信代理進行交互的方式實現。
不幸的是,目前的提案沒有給予這種靈活性:
class A {
#priv;
method() {
this.#priv; // brand-check ALWAYS happens here
}
}
複製代碼
若是在沒有用A
的構造函數構建的對象的上下文中調用method
方法,該方法將始終拋出異常。這就是最可怕的事實:brand-check
在這裏隱含並與另外一個特徵——「封裝」混合。
雖然幾乎全部類型的代碼都須要封裝,但品牌檢查的用例數量很是有限。 當開發人員想要隱藏實現細節時,將它們混合成一種語法將致使意外的brand-check
,而爲了推廣這個proposal,宣傳#是新的_
更是雪上加霜。
您還能夠閱讀有關當前提案破壞代理行爲的討論細節。 在Aurelia開發者和Vue.js做者參與其中。此外,個人評論描述了代理的幾個用例之間的差別,這可能頗有趣。 並討論了私有字段與
膜
模式之間的關係。
除非有其餘選擇,不然全部這些討論都沒有多大意義。 不幸的是,它們都沒有達到第一階段,所以這些備選提案沒有機會獲得充分發展。 可是,我想指出其中的一些,以某種方式解決上述問題。
private
做爲對象使用看起來我彷佛在責怪委員會——但實際上,我沒有。 我只是認爲,爲了在JS中實現適當的封裝,已經通過多年(甚至幾十年,取決於選擇的起點)的努力,而咱們業界更是的發生了不少變化,可能委員會錯過了一些變化。致使,相關事體的優先級別可能會變得有些模糊。
不光如此,咱們做爲一個社區,迫使TC39更快地發佈功能,可是咱們卻沒有爲早期提案提供足夠的反饋,結果致使爭論不少而可以用來改變某些事情的時間不多。
有觀點認爲,在這種狀況下,該提案過程失敗了。
在長期潛水以後,我決定盡我所能,以防止未來發生這種狀況。 不幸的是,我作不了多少——只能寫寫評論文章並在babel
中實現早期提案。
總的來講,反饋和溝通是最重要的,因此我懇請你們與委員會分享更多的想法。