使用 babel 外掛來打造真正的“私有”屬性

發表於2017-04-19

大家都知道 JavaScript 的物件屬性預設是可以被從外部訪問和修改的,也就是說,JavaScript 本身不存在完全“私有”的物件屬性。例如:

在上面的程式碼裡,我們約定俗成地用下劃線開頭來表示私有變數。我們希望 _x、_y 不被外部訪問,然而,這只是我們一廂情願,使用者還是可以訪問到這兩個變數。

在這裡,我們不討論 ES 的 private 標準提案,而是討論如何使用工具來將約定變成真正的私有。

使用 Symbol 來構造私有資料

ES6 提供了一個新的資料型別叫做 Symbol,Symbol 有許多用途,其中一個用途是可以用來生成唯一 key,用作屬性標識,我們利用它可以實現真正的私有屬性:

我們改寫上一版的程式碼,用 Symbol 的 _x、_y 代替字串來作為 key,這樣,外部 p 訪問 _x、_y 屬性就訪問不到了,這樣我們就真正實現了物件資料的私有。

上面這種用法並不複雜,但是,如果我們每次定義物件都這麼去寫還是顯得麻煩。因此,我們可以考慮讓編譯器去做這件事情,自動將下劃線開頭的屬性編譯成私有屬性。

使用 Babel 外掛來實現屬性的預設私有

在這裡,我們可以開發 Babel 的外掛來實現。Babel 的原理在部落格之前的文章中有介紹。還有使用 Babel 外掛來進行測試覆蓋度檢查的例子。如果對於 Babel 不熟悉的同學,可以回顧一下之前的文章。

首先,我們分析一下要處理的 AST 部分。ES6 的 class 有兩種 node 型別,一種是 ClassDeclaration,另一種是 ClassExpression。它們比較類似,但是在一些細節上又有區別。比如 ReturnStatement 之後可以跟 ClassExpression 但是不能跟 ClassDeclaration。

ClassDeclaration 與 ClassExpression

對這兩種 node,如果其中有下劃線開頭的屬性,可以分別編譯成如下形式:

此外,還需要考慮 ES Modules 的情況:

對應為:

上面的形式沒有問題。但是如果:

對應為:

編譯會報錯。因此要進行修改,對應成:

由於 Class 允許存在巢狀,因此,我們需要使用堆疊結構,在 AST 的 enter 的時候建立存放當前 Class 的 scope 下的私有屬性列表。堆疊還有一個作用,就是如果堆疊為空,那麼當前作用域不在 Class 內部,不用進行編譯轉換。

接下來,我們處理具體的 Identifier:

Protected 的屬性和 super._x 操作

對於物件方法帶下劃線的情況,和 this 帶下劃線不同,我們是可以使用 super.屬性名 來訪問的。比如:

在這裡,我們需要對 super._X 進行處理,如果直接編譯:

由於每個 Symbol 都是唯一的,所以 Bar 的 Symbol(‘$_X$’) 和 Foo 的並不相同,這樣也就獲取不到 super[$_X$] 實際的值了。

因此,在這裡,我們編譯的時候不能直接這樣轉成 Symbol,而是要通過反射機制去處理:

上面的 super 裡的 key 有一大串,是:

這裡通過 Object.getOwnPropertySymbols(this.__proto__.__proto__) 反射出父類的 Symbol,然後通過字串匹配到對應的 key。

於是,我們確定了轉換方法,那麼接下來就只是實現具體的轉換細節了:

上面的方法將 ClassDeclaration 和 ClassExpression 處理完成。接下來是處理 super 屬性的部分:

上面程式碼雖然繁瑣,但都並不複雜,只是 AST 樹的構建而已。最終,我們形成完整的外掛程式碼。有興趣的同學可以關注這個 GitHub repo

要使用的話,直接安裝:

然後配置一下:

其中配置的 pattern 引數可以修改私有變數的匹配正規表示式,預設是 `”^_” 也就是以下劃線開頭,可以改成別的模式。

以上就是今天的全部內容,程式碼比較多,但是關鍵點就這些,其他就是構建 AST 樹的過程。如有任何問題,歡迎討論。

相關文章