在上一篇 某跳動面試官:說說微信掃碼登陸背後的實現原理? 文章發出以後,沒想到有挺多點讚的,掘金社區前端小夥伴真是多啊,之後仍是多多在掘金社區活躍起來吧,簡單說一下我會整理的專欄系列:javascript
以前,一直在 CSDN
平臺發佈博客,超逸の學習技術博客,發現前端活躍度不是很高,而在掘金社區我看到一系列優秀的文章,點贊數達到上千,訪問量好幾十萬的也有,而且文章質量是真的高,能學習不少知識。前端
在這裏,我會對一個問題進行研究,帶着好奇心去看待問題,儘可能用簡潔易懂的話語呈現給你們,能把別人教會,對於本身而言也是蠻有成就感和收穫的。java
在此,分享一下上學期IT項目管理老師教課提到的人們可以記住的東西有以下規律:git
如如有幫助到您,請一鍵三連,固然,本文表述有問題的地方,歡迎讀者指正,也是一個學習的過程,謝謝~es6
趕忙回到正題,這個問題也是和上一篇博客同樣,也是在今年8月份的時候被問到過,當時知道class這個東西,在社區裏面看過一些class繼承相關知識,可是沒有真正動手敲過代碼,猶記得當時對話場景是這樣的:github
面試官:你應該瞭解過ES6吧?(這個固然),那好,那你知道ES6中有一個class,你能夠設計實現它的私有屬性嗎?面試
我:emmm(此時我想了想,好像能夠用閉包來作),我能夠採用閉包的思想來作嘛?編程
面試官:固然能夠(show me the code)微信
因而乎,我就寫下了這一份代碼:markdown
class classA{
// xxx省略
let fun = function () {
var a = 0;
return function () {
console.log(++a);
}
}
// xxx省略
}
複製代碼
其中省略了一點點代碼,但整體和上述代碼差很少,如今回想過來,當時真是太好笑了,難怪面試官喊停來了一句,語法都不對。同時,當時寫的時候也是焦頭爛額的,由於這個語法代碼也不是很熟,但今天我帶着好奇心來解決這個問題。
其實,學過 java
的小夥伴必定對 class
熟悉不過了,本人大二大三期間也是各類 java
代碼寫來寫去。那爲何 JS
裏面還要引入 class
呢?
在 es6 以前,雖然 JS 和 Java 一樣都是 OOP (面向對象)語言,可是在 JS 中,只有對象而沒有類的概念。
es6 中 class 的出現拉近了 JS 和傳統 OOP 語言的距離。可是,它僅僅是一個語法糖罷了,不能實現傳統 OOP 語言同樣的功能。在其中,比較大的一個痛點就是私有屬性問題。
私有屬性是面向對象編程(OOP)中很是常見的一個特性,通常知足如下的特色:
在 Java 中,可使用 private
實現私有變量,可是惋惜的是, JS 中並無該功能。
2015年6月,ES6發佈成爲標準,爲了記念這個歷史性時刻,這個標準又被稱爲ES2015,至此,JavaScript中的 class 從備胎中轉正。可是沒有解決私有屬性這個問題,產生了一個提案——在屬性名以前加上 #
,用於表示私有屬性。
class Foo {
#a; // 定義私有屬性
constructor(a, b) {
this.#a = a;
this.b = b
}
}
複製代碼
上述代碼私有屬性的聲明,須要先通過Babel等編譯器編譯後才能正常使用。
至於爲何不用 private
關鍵字呢?參考大佬說的就是有一大緣由是向 Python 靠攏,畢竟從 es6 以來, JS 一直向着 Python 發展。
上文咱們介紹了class 出現緣由,以及它沒有解決私有屬性這個問題,那麼咱們做爲 JSer
們,如何本身設計一下呢?帶着好奇心來探討一下吧:
目前使用最廣的方式:約定命名,既然尚未解決,咱們不是能夠本身定義一下嘛,對於特殊命名的就把它當作私有屬性使用不就能夠了嗎?你們都遵循這個規範,不就解決這個問題了嗎?
/* 約定命名 */
class ClassA {
constructor(x) {
this._x = x;
}
getX() {
return this._x;
}
}
let classa = new ClassA(1);
/* 此時能夠訪問咱們自定義私有屬性命名的_x */
console.log(classa._x); // 1
console.log(classa.getX()); // 1
複製代碼
顯然,上述方法簡單方便,你們按照規範來就能夠了,也比較好閱讀他人代碼。
閉包的一個好處就是能夠保護內部屬性,也是我開頭想要實現的一種方式,作法就是將屬性定義在 constructor
做用域內,以下代碼:
/* 閉包 */
class ClassB {
constructor(x) {
let _x = x;
this.getX = function(){
return _x;
}
}
}
let classb = new ClassB(1);
/* 此時不能夠訪問咱們自定義私有屬性命名的_x */
console.log(classb._x); // undefined
console.log(classb.getX()); // 1
複製代碼
顯然,若是私有屬性愈來愈多,那麼看起來就很臃腫,對後續維護形成了必定的麻煩,對於他人閱讀也是不太友好。同時呢,引用私有變量的方法又不能定義在原型鏈上。
能夠經過 IIFE
(當即執行函數表達式) 創建一個閉包,在其中創建一個變量以及 class ,經過 class 引用變量實現私有變量。
/* 進階版閉包 */
const classC = (function () {
let _x;
class ClassC {
constructor(x) {
_x = x;
}
getX() {
return _x;
}
}
return ClassC;
})();
let classc = new classC(3);
/* 此時不能夠訪問咱們自定義私有屬性命名的_x */
console.log(classc._x); // undefined
console.log(classc.getX()); // 3
複製代碼
這種方式就有點 模塊化 的思想了,關於模塊化的知識,推薦以前的這篇文章:
「查漏補缺」深度剖析JavaScript ES5/AMD/CMD/COMMONJS/ES6模塊化(加薪必備)| 掘金技術徵文-雙節特別篇
上述,咱們用了閉包和進階版閉包來解決私有屬性這個問題,可是這是有問題的,咱們以進階版閉包爲例:
/* 進階版閉包帶來的問題 */
const classC = (function () {
let _x;
class ClassC {
constructor(x) {
_x = x;
}
getX() {
return _x;
}
}
return ClassC;
})();
let classc1 = new classC(3);
/* 此時不能夠訪問咱們自定義私有屬性命名的_x */
console.log(classc1._x); // undefined
console.log(classc1.getX()); // 3
/* 問題引出:此時新建立一個實例 */
let classc2 = new classC(4);
/* 出現了問題:實例之間會共享變量 */
console.log(classc1.getX()); // 4
複製代碼
從上述代碼能夠發現,用閉包建立私有變量是不行的,實例之間會共享變量,就好像幾我的都實例化了,可是操做地仍是同一個屬性,這顯然是不可取的。
利用 Symbol
變量能夠做爲對象 key
的特色,咱們能夠模擬實現更真實的私有屬性。
/* Symbol */
const classD = (function () {
const _x = Symbol('x');
class ClassD {
constructor(x) {
this[_x] = x;
}
getX() {
return this[_x];
}
}
return ClassD;
})();
let classd = new classD(4);
/* 此時不能夠訪問咱們自定義私有屬性命名的_x */
console.log(classd._x); // undefined
console.log(classd.getX()); // 4
classd[_x] = 1;
console.log(classd[_x]); // ReferenceError: _x is not defined
複製代碼
關於上述代碼,我參考了大佬文章底下評論區的回答:
Sysmol要配合 import/export 模板語法。好比A.js裏面你定義了class A和Symbol(就用你的寫法),對外只暴露class A。而後在別的js文件引入class A實例化,拿不到Symbol的值,並且沒法經過'.'去訪問變量名(Symbol惟一,不暴露外界拿不到)。這樣纔是私有。
經過模板化的角度,咱們對外暴露 ClassD
,Symbol
惟一,不會暴露,外界拿不到,可是這個也不是毫無破綻,看以下代碼:
console.log(classd[Object.getOwnPropertySymbols(classd)[0]]); // 4
複製代碼
原來,ES6 的 Object.getOwnPropertySymbols
能夠獲取symbol屬性,今天又學到了新東西 (*^▽^*)
爲了解決上述問題,咱們又要引出一個新的東西:WeakMap
/* WeakMap */
const classE = (function () {
const _x = new WeakMap();
class ClassE {
constructor(x) {
_x.set(this, x);
}
getX() {
return _x.get(this);;
}
}
return ClassE;
})();
let classe = new classE(5);
/* 此時不能夠訪問咱們自定義私有屬性命名的_x */
console.log(classe._x); // undefined
console.log(classe.getX()); // 5
複製代碼
這種方式就很好解決了私有屬性的問題,至於 WeakMap 和 Map
相關知識,我打算在下一篇文章繼續探討,這個知識目前也不算是特別瞭解,大概瞭解不能遍歷、弱引用這些,能夠關注後續的文章。
10月12日補充更新
在評論區@HsuYang 小夥伴的提出的問題:若是是要支持多個私有變量的話,這兒用Map有沒有啥問題呢?
因而我就嘗試了一下多個私有變量,先看以下代碼:
/* WeakMap */
const classE = (function () {
const _x = new WeakMap();
class ClassE {
constructor(x, y) {
_x.set(this, x);
_x.set(this, y);
}
getX() {
return _x.get(this);;
}
}
return ClassE;
})();
let classe = new classE(5, 6);
/* 此時不能夠訪問咱們自定義私有屬性命名的_x */
console.log(classe.getX()); // 6
複製代碼
誒,發現問題了沒有,咱們最後輸出的只有 _y
這個私有屬性,原來出現了覆蓋問題,那麼該如何解決這個問題呢?
既然私有屬性要和實例進行關聯,那麼是否是能夠建立一個包含全部私有屬性對應的對象來維護呢?這樣全部私有屬性就都存儲在其中了,也就解決多個私有變量問題啦,同時,這種技術也有好處,就是在遍歷屬性時或者在執行 JSON.stringify
時不會展現出實例的私有屬性。
但它依賴於一個放在類外面的能夠訪問和操做的 WeakMap
變量。
const map = new WeakMap();
// 建立一個在每一個實例中存儲私有變量的對象
const internal = (obj) => {
if (!map.has(obj)) {
map.set(obj, {});
}
return map.get(obj);
}
class ClassE {
constructor(name, age) {
internal(this).name = name;
internal(this).age = age;
}
get userInfo() {
return '姓名:' + internal(this).name + ',年齡:' + internal(this).age;
}
}
const classe1 = new ClassE('Chocolate', 18);
const classe2 = new ClassE('Lionkk', 19);
console.log(classe1.userInfo); // 姓名:Chocolate,年齡:18
console.log(classe2.userInfo); // 姓名:Lionkk,年齡:19
/* 沒法訪問私有屬性 */
console.log(classe1.name); // undefined
console.log(classe2.age); // undefined
複製代碼
在評論區@蜀 黍 小夥伴提出能夠用 代理設置攔截 這種方式來作,如今來補充一下。
Proxy 是 JavaScript 中一項美妙的新功能,它將容許你有效地將對象包裝在名爲 Proxy 的對象中,並攔截與該對象的全部交互。咱們將使用 Proxy 並遵守上面的 命名約定 來建立私有變量,但可讓這些私有變量在類外部訪問受限。
Proxy 能夠攔截許多不一樣類型的交互,但咱們要關注的是 get
和 set
,Proxy 容許咱們分別攔截對一個屬性的讀取和寫入操做。建立 Proxy 時,你將提供兩個參數,第一個是打算包裹的實例,第二個是您定義的但願攔截不一樣方法的 「處理器」 對象。
咱們的處理器將會看起來像是這樣:
const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
}
};
複製代碼
在每種狀況下,咱們都會檢查被訪問的屬性的名稱是否如下劃線開頭,若是是的話咱們就拋出一個錯誤從而阻止對它的訪問。
經過以上方法保留使用 instanceof
的能力(閉包那一塊就出現了這個問題),可是此時又有一個新的問題:
當咱們嘗試執行 JSON.stringify
時會出現問題,由於它試圖對私有屬性進行格式化。爲了解決這個問題,咱們須要重寫 toJSON
函數來僅返回「公共的」屬性。咱們能夠經過更新咱們的 get
處理器來處理 toJSON
的特定狀況:
注:這將覆蓋任何自定義的 toJSON 函數。
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
} else if (key === 'toJSON') {
const obj = {};
for (const key in target) {
if (key[0] !== '_') { // 只複製公共屬性
obj[key] = target[key];
}
}
return () => obj;
}
return target[key];
}
複製代碼
那麼咱們就能夠整合一下代碼了:
class Student {
constructor(name, age) {
this._name = name;
this._age = age;
}
get userInfo() {
return '姓名:' + this._name + ',年齡:' + this._age;
}
}
const handler = {
get: function (target, key) {
if (key[0] === '_') { // 訪問私有屬性,返回一個 error
throw new Error('Attempt to access private property');
} else if (key === 'toJSON') {
const obj = {};
for (const key in target) { // 只返回公共屬性
if (key[0] !== '_') {
obj[key] = target[key];
}
}
return () => obj;
}
return target[key]; // 訪問公共屬性,默認返回
},
set: function (target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
}
}
const stu = new Proxy(new Student('Chocolate', 21), handler);
console.log(stu.userInfo); // 姓名:Chocolate,年齡:21
console.log(stu instanceof Student); // true
console.log(JSON.stringify(stu)); // "{}"
for (const key in stu) {
console.log(key); // _name _age
}
複製代碼
咱們如今已經封閉了咱們的私有屬性,而預計的功能仍然存在,惟一的警告是咱們的私有屬性仍然可被遍歷。for(const key in stu)
會列出 _name
和 _age
。
爲了解決上述私有屬性遍歷問題,我又想到了能夠操做對象屬性對應的屬性描述符,而後配置 enumerable
,正好 Proxy
能夠處理這個問題,它能夠攔截對 getOwnPropertyDescriptor
的調用並操做咱們的私有屬性的輸出,代碼以下:
getOwnPropertyDescriptor(target, key) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (key[0] === '_') {
desc.enumerable = false;
}
return desc;
}
複製代碼
詳細內容可參考:
Object.getOwnPropertyDescriptor 參考文檔
終於,咱們迎來了最終完整版本,祝賀 (*^▽^*)
,整合代碼以下:
class Student {
constructor(name, age) {
this._name = name;
this._age = age;
}
get userInfo() {
return '姓名:' + this._name + ',年齡:' + this._age;
}
}
const handler = {
get: function (target, key) {
if (key[0] === '_') { // 訪問私有屬性,返回一個 error
throw new Error('Attempt to access private property');
} else if (key === 'toJSON') {
const obj = {};
for (const key in target) { // 只返回公共屬性
if (key[0] !== '_') {
obj[key] = target[key];
}
}
return () => obj;
}
return target[key]; // 訪問公共屬性,默認返回
},
set: function (target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
},
// 解決私有屬性能遍歷問題,經過訪問屬性對應的屬性描述符,而後設置 enumerable 爲 false
getOwnPropertyDescriptor(target, key) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (key[0] === '_') {
desc.enumerable = false;
}
return desc;
}
}
const stu = new Proxy(new Student('Chocolate', 21), handler);
console.log(stu.userInfo); // 姓名:Chocolate,年齡:21
console.log(stu instanceof Student); // true
console.log(JSON.stringify(stu)); // "{}"
for (const key in stu) { // No output 不能遍歷私有屬性
console.log(key);
}
stu._name = 'Lionkk'; // Error: Attempt to access private property
複製代碼
就發展趨勢來看, TS 已經成爲前端必備的技能之一,TypeScript
的 private 很好解決了私有屬性這個問題,後續學習了 ts
以後再補充吧。
TypeScript 是 JavaScript 的一個超集,它會編譯爲原生 JavaScript 用在生產環境。容許指定私有的、公共的或受保護的屬性是 TypeScript 的特性之一。
class Student {
private name;
private age;
constructor(name, age) {
this.name = name;
this.age = age;
}
get userInfo() {
return '姓名:' + this.name + ',年齡:' + this.age;
}
}
const stu = new Student('Chocolate', 21);
console.log(stu.userInfo); // 姓名:Chocolate,年齡:21
複製代碼
使用 TypeScript 須要注意的重要一點是,它只有在 編譯 時才獲知這些類型,而私有、公共修飾符在編譯時纔有效果。若是你嘗試訪問 stu.name
,你會發現,竟然是能夠的。只不過 TypeScript 會在編譯時給你報出一個錯誤,但不會中止它的編譯。
// 編譯時錯誤:屬性 ‘name’ 是私有的,只能在 ‘Student ’ 類中訪問。
console.log(stu.name); // 'Chocolate'
複製代碼
TypeScript 不會自做聰明,不會作任何的事情來嘗試阻止代碼在運行時訪問私有屬性。我只把它列在這裏,也是讓你們意識到它並不能直接解決問題。
另外,TypeScript 的 class 私有變量最終編譯也是經過 WeakMap
來實現的,來自評論區小夥伴們的解答~
到此,本文就結束了,後續的文章也會加快更近,帶着好奇心去學習,去思考~
如若小夥伴有更加不錯的方式,歡迎交流,固然,本文或許存在疑點,歡迎你們指正,也是一個學習的過程,謝謝~
特此感謝評論區的小夥伴們,對於 設計一下ES6中 class 實現私有屬性 這個問題我又有了更深刻的理解,感謝 Thanks♪(・ω・)ノ
感謝以上大佬的文章,尊重勞動成果,特此提出原文連接。
文章產出不易,還望各位小夥伴們支持一波!
往期精選:
leetcode-javascript:LeetCode 力扣的 JavaScript 解題倉庫,前端刷題路線(思惟導圖)
小夥伴們能夠在Issues中提交本身的解題代碼,🤝 歡迎Contributing,可打卡刷題,Give a ⭐️ if this project helped you!
訪問超逸の博客,方便小夥伴閱讀玩耍~
學如逆水行舟,不進則退
複製代碼