JavaScript被很多人認為並不是一種面向對象語言,原因有很多種,比如JavaScript沒有類,不能提供傳統的類式繼承;再比如JavaScript不能實現信息的隱藏,不能實現私有成員。本文並不是為了打破以上誤解(實際上筆者自己也有困惑),只是簡單介紹幾種JavaScript實現私有屬性的方式,以及各自的優劣。
1. 基於編碼規范約定實現方式
很多編碼規范把以下划線_
開頭的變量約定為私有成員,便於同團隊開發人員的協同工作。實現方式如下:
function Person(name){
this._name = name;
}
var person = new Person('Joe');
這種方式只是一種規范約定,很容易被打破。而且也並沒有實現私有屬性,上述代碼中的實例person可以直接訪問到_name
屬性:
alert(person._name); //'Joe'
2. 基於閉包的實現方式
另外一種比較普遍的方式是利用JavaScript的閉包特性。構造函數內定義局部變量和特權函數,其實例只能通過特權函數訪問此變量,如下:
function Person(name){
var _name = name;
this.getName = function(){
return _name;
}
}
var person = new Person('Joe');
這種方式的優點是實現了私有屬性的隱藏,Person 的實例並不能直接訪問_name
屬性,只能通過特權函數getName獲取:
alert(person._name); // undefined
alert(person.getName()); //'Joe'
使用閉包和特權函數實現私有屬性的定義和訪問是很多開發者采用的方式,Douglas Crockford也曾在博客中提到過這種方式。但是這種方式存在一些缺陷:
- 私有變量和特權函數只能在構造函數中創建。通常來講,構造函數的功能只負責創建新對象,方法應該共享於prototype上。特權函數本質上是存在於每個實例中的,而不是prototype上,增加了資源占用。
3. 基於強引用散列表的實現方式
JavaScript不支持Map數據結構,所謂強引用散列表方式其實是Map模式的一種變體。簡單來講,就是給每個實例新增一個唯一的標識符,以此標識符為key,對應的value便是這個實例的私有屬性,這對key-value保存在一個Object內。實現方式如下:
var Person = (function() {
var privateData = {},
privateId = 0;
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());
上述代碼的有以下幾個特征:
- 使用自執行函數創建Person類,變量privateData和privateId被所有實例共享;
- privateData用來儲存每個實例的私有屬性name的key-value,privateId用來分配每個實例的唯一標識符
_id
; - 方法getName存在於prototype上,被所有實例共享。
這種方式在目前ES5環境下,基本是最佳方案了。但是仍然有一個致命的缺陷:散列表privateData對每個實例都是強引用,導致實例不能被垃圾回收處理。如果存在大量實例必然會導致memory leak。
造成以上問題的本質是JavaScript的閉包引用,以及只能使用字符串類型最為散列表的key值。針對這兩個問題,ES6新增的WeakMap可以良好的解決。
4. 基於WeakMap的實現方式
WeakMap有以下特點:
- 支持使用對象類型作為key值;
- 弱引用。
根據WeakMap的特點,便不必為每個實例都創建一個唯一標識符,因為實例本身便可以作為WeakMap的key。改進后的代碼如下:
var Person = (function() {
var privateData = new WeakMap();
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());
改進的代碼不僅僅干凈了很多,而且WeakMap是一種弱引用散列表, 這意味着,如果沒有其他引用和該鍵引用同一個對象,這個對象將會被當作垃圾回收掉。解決了內存泄露的問題。
不幸的是,目前瀏覽器對WeakMap的支持率並不理想,投入生產環境仍然需要等待。