王國維在《人間詞話》里談到了治學經驗,他說:“古今之成大事業、大學問者,必經過三種之境界。”
巧合的是,最近受 git chat / git book 邀請,做了一個分享。
其中談到JS中凍結一個對象幾種由淺入深的實踐。想想也暗合國學大師所謂的三重境界。 具體分享內容,還沒有上車的同學們可以點擊這里,參看實錄。歡迎一起討論。相關文章,點擊這里也可以查看。
這篇文章由淺入深討論JS中對象的一些鎖定特性。但都是一些基礎語法的實現,相信即便是前端小白也可以大體領會。不過需要讀者預先了解JS中對象的特性,尤其是對象自身屬性的描述符:configurable、writable...
另外,如果您對JS中對象操作、不可變數據、函數式編程感興趣,同樣推薦我的其他一些相關文章:
等等。
昨夜西風凋碧樹 獨上高樓 望盡天涯路
第一種境界:“昨夜西風凋碧樹,獨上高樓,望盡天涯路。”
該詞句出自晏殊的《蝶戀花》,原意是說,“我”上高樓眺望所見的更為蕭颯的秋景,西風黃葉,山闊水長,案書何達?
王國維此句中解成:做學問成大事業者,首先要有執著的追求,登高望遠,瞰察路徑,明確目標與方向,了解事物的概貌。
我們就從最基本的場景說起,究竟為什么要凍結一個對象?
場景一:
我們造了一個輪子,對外暴露一個對象,開放出來給第三方使用。同時需要保證這個對外暴露的對象完全安全,不能被業務代碼所改寫覆蓋或下鉤子(hook)函數。場景二:
如果你看過Vue 2.* 版本源碼,你會發現凍結一個對象的操作頻繁出現。
我們先來看凍結對象的第一層實現 —— 擴展特性鎖:
他包含了兩個基本方法:
- Object.isExtensible
- Object.preventExtensions
如果一個對象可以添加新的屬性,則這個對象是可擴展的。擴展特性鎖就是讓這個對象變的不可擴展,也就是不能再有新的屬性。
Object.isExtensible
MDN上內容概述:
概述
Object.isExtensible() 方法判斷一個對象是否是可擴展的(是否可以在它上面添加新的屬性)。
語法
Object.isExtensible(obj)
參數
obj 需要檢測的對象
例如,我們正常使用對象字面量聲明的對象都是可擴展的:
var person1 = {};
person1.name = "Lucas";
console.log(person1);
// {name: "Lucas"}
同時:
Object.isExtensible(person1) === true; // true
你可能要問了,那么使用Object.create方法聲明的對象,并對該對象屬性進行配置是什么情況呢?
我們知道,用上面對象字面量聲明的對象相當于:
var person1 = Object.create({},{
"name":{
value : "Lucas",
configurable : true, //可配置
enumerable : true , //可枚舉
writable : true //可寫
}
});
即便嘗試將configurable設置為false:
var person1 = Object.create({},{
"name":{
value : "Lucas",
configurable : false, //不可配置
enumerable : true, //可枚舉
writable : true //可寫
}
});
仍然得到:
Object.isExtensible(person1) === true; // true
Object.preventExtensions
當然,我們還是有方法可以使得一個對象變的不可擴展。
MDN上內容概述:
概述
Object.preventExtensions() 方法讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。
語法
Object.preventExtensions(obj)
參數
obj 將要變得不可擴展的對象
幾個注意點包括但不限于:
- 不可擴展的對象的屬性通常仍然可以被刪除。
- 嘗試給一個不可擴展對象添加新屬性的操作將會失敗,不過可能是靜默失敗,也可能會拋出 TypeError 異常(嚴格模式下)。
- Object.preventExtensions 只能阻止一個對象不能再添加新的自身屬性,仍然可以為該對象的原型添加屬性。
比如:
var person1 = {
name: "Lucas"
}
Object.preventExtensions(person1);
person1.age = 18;
// 非嚴格模式下,這里不會有報錯,屬于靜默失敗
person1.age // undefined
// 擴展新屬性失敗了
仍然可以向原型鏈添加屬性:
person1.__proto__.age = 18;
person1.age // 18
// 可以從原型鏈上取到
同樣也可以復寫一些屬性:
person1.name = "Eros";
person1.name // "Eros"
也可以刪除已有屬性:
person1.name; // "Eros",
delete person1.name;
person1.name; // undefined
通過以上方法,我們實現了對一個對象屬性擴展的凍結。但是同樣也認識到,這并不是全面的保護:例如可以隨意改動去覆蓋已有屬性,在對象原型鏈上增加屬性也還是難以屏蔽。
衣帶漸寬終不悔 為伊消得人憔悴
第二種境界:“衣帶漸寬終不悔,為伊消得人憔悴。”
這引用的是北宋柳永《蝶戀花》最后兩句詞,原詞是表現作者對愛的艱辛和愛的無悔。若把“伊”字理解為詞人所追求的理想和畢生從事的事業,亦無不可。王國維則別有用心,以此兩句來比喻成大事業、大學問者,不是輕而易舉,隨便可得的,必須堅定不移,經過一番辛勤勞動,廢寢忘食,孜孜以求,直至人瘦帶寬也不后悔。
下面介紹一個更深一層的做法:密封特性。
密封對象是指那些不能添加新的屬性,不能刪除已有屬性,以及不能修改已有屬性的可枚舉性(enumerable)、可配置性(configurable)、可寫性(writable),但可能可以修改已有屬性的值的對象。
他同樣包含了兩個基本方法:
- Object.isSealed
- Object.seal
Object.isSealed
MDN上內容概述:
概述
Object.isSealed() 方法判斷一個對象是否是密封的(sealed)。
語法
Object.isSealed(obj)
參數
obj 將要檢測的對象
正常對象字面量聲明的對象是不被密封的:
var person1 = {
name: "Lucas"
}
Object.isSealed(person1); // false
當將這個對象禁止擴展時,它也不會變成密封的:
var person1 = {
name: "Lucas"
}
Object.preventExtensions(person1);
Object.isSealed(person1); // false
但是在此基礎上,使用Object.defineProperty方法,把屬性變得不可配置(configurable),則這個對象也就成了密封對象:
var person1 = {
name: "Lucas"
}
Object.defineProperty(person1, "name", {configurable : false});
Object.isSealed(person1); // true
此時,我們有:
Object.getOwnPropertyDescriptor(person1, 'name');
// 得到:
Object {
value: "Lucas",
writable: true,
enumerable: true,
configurable: false
}
根據這個getOwnPropertyDescriptor,我們可以更加深入的理解密封特性:被密封的對象,就是在不可擴展基礎上講屬性描述符configurable設置為false; 同時,被密封的對象,仍然有機會改變屬性的值。只不過對于此對象本身而言,不可以再擴展新的屬性,不可以更改已有屬性的配置信息。
Object.seal
相對應我們也有一個方法將一個對象密封。
MDN上內容概述:
概述
Object.seal() 方法可以讓一個對象密封,并返回被密封后的對象。
語法
Object.seal(obj)
參數
obj 將要被密封的對象
比如:
var person1 = {
name: "Lucas"
}
Object.getOwnPropertyDescriptor(person1, 'name');
// 得到:
Object {
value: "Lucas",
writable: true,
enumerable: true,
configurable: true
}
將此對象密封后:
Object.seal(person1);
Object.getOwnPropertyDescriptor(person1, 'name');
// 得到:
Object {
value: "Lucas",
writable: true,
enumerable: true,
configurable: false
}
也就是說:
person1.age = 18;
person1.age; // undefined
// 擴展新屬性失敗
// 同時調用defineProperty失敗
Object.defineProperty(person1,"name",{get : function(){return "g";}});
// 拋出異常
任何除更改屬性值以外的操作,非嚴格模式下都會靜默失敗,如上并如下:
delete person1.name;
person1.name; // "Lucas"
而更改屬性值可以成功:
person1.name = "Eros";
person1.name; // "Eros"
怎么理解這樣的現象呢?牢記,被密封的對象擁有如下的屬性描述符:
Object {
value: "Lucas",
writable: true,
enumerable: true,
configurable: false
}
而刪除屬性屬于configurable,更改屬性才屬于writable;
一點延伸
借助于此,我們其實已經可以完成凍結對象的第三重境界:達到即密封又不可修改原屬性值。因為可以這樣做:
var person1 = {name: "Lucas"};
Object.defineProperty(person1, "name", {configurable: false, writable: false});
Object.preventExtensions(person1);
總結下就是設置:
configurable: false + writable: false + preventExtensions
或者因為
configurable: false+ preventExtensions = seal
所以也可以設置:
seal + writable: false
眾里尋他千百度,驀然回首,那人卻在,燈火闌珊處
第三種境界:“眾里尋他千百度,驀然回首,那人卻在,燈火闌珊處。”
這是引用南宋辛棄疾《青玉案》詞中的最后四句。梁啟超稱此詞“自憐幽獨,傷心人別有懷抱”。這是借詞喻事,與文學賞析已無交涉。王國維已先自表明,“吾人可以無勞糾葛”。他以此詞最后的四句為“境界”之第三,即最終最高境界。
這雖不是辛棄疾的原意,但也可以引出悠悠的遠意:做學問、成大事業者,要達到第三境界,必須有專注的精神。反復追尋、研究,下足功夫,自然會豁然貫通,有所發現,有所發明,就能夠從必然王國進入自由王國。
上邊那種凍結對象的方法,其實也有原生實現,可謂:“眾里尋他千百度,驀然回首,那人卻在,燈火闌珊處”
我們這里所說的一個對象的凍結(frozen)是指它不可擴展,所有屬性都是不可配置的(non-configurable),且所有數據屬性(data properties)都是不可寫的(non-writable)。
或者說,凍結對象是指那些不能添加新的屬性,不能修改已有屬性的值,不能刪除已有屬性,以及不能修改已有屬性的可枚舉性、可配置性、可寫性的對象。也就是說,這個對象永遠是不可變的。
同樣,包含了兩個基本方法:
- Object.isFrozen
- Object.freeze
Object.isFrozen
MDN上內容概述:
概述
Object.isFrozen() 方法判斷一個對象是否被凍結(frozen)。
語法
Object.isFrozen(obj)
參數
obj 被檢測的對象
Object.freeze 方法
MDN上內容概述:
概述
Object.freeze() 方法可以凍結一個對象。
語法
Object.freeze(obj)
參數
obj 將要被凍結的對象
可以先理解為,這是最高一層的凍結對象:
var person1 = {
name: "Lucas"
}
Object.freeze(person1);
此時,我們有:
Object.getOwnPropertyDescriptor(person1, 'name')
Object {
value: "Lucas",
writable: false,
enumerable: true,
configurable: false
}
// 對凍結對象的任何操作都會失敗
person1.name = "Eros"; // 改寫屬性值,非嚴格模式下靜默失敗;
person1.age = 18; // 擴展屬性值,非嚴格模式下靜默失敗;
Object.defineProperty(person1,"name",{value: "Eros"}); // 使用defineProperty會直接報錯
改寫屬性值,擴展新屬性,調用defineProperty,全部都會失敗。
但是,這種層面的凍結,只是淺凍結。如果對象里面還嵌套有對象,那么這個內部對象絲毫不受影響。
var person1 = {
name: "Lucas",
family: {
brother: "Eros"
}
}
Object.freeze(person1);
person1.family.brother = "Tim";
person1.family.brother // "Tim"
終極實現
那么,如果我們想深層次凍結一個對象呢?思路和深拷貝暗合,使用遞歸:
Object.prototype.deepFreeze = Object.prototype.deepFreeze || function (o){
var prop, propKey;
Object.freeze(o); // 首先凍結第一層對象
for (propKey in o){
prop = o[propKey];
if(!o.hasOwnProperty(propKey) || !(typeof prop === "object") || Object.isFrozen(prop)){
continue;
}
deepFreeze(prop); // 遞歸
}
}
這樣子,我們再回過頭來看:
var person1 = {
name: "Lucas",
family: {
brother: "Eros"
}
}
Object.deepFreeze(person1);
person1.family.brother = "Tim";
person1.family.brother // "Eros"
已經達到了深層次對象屬性的凍結。
總結
本文先后介紹了關于凍結一個對象的三種進階方法。他們層層遞進,卻又相互關聯。關系如圖:
文章部分概念粘取了MDN語法介紹和Tomson的文章。
在《文學小言》一文中,王國維把上述三境界說成“三種之階級”。并說:“未有不閱第一第二階級而能遽躋第三階級者,文學亦然。此有文學上之天才者,所以又需莫大之修養也。”
與大家共勉。
Happy coding!
PS: 作者Github倉庫,歡迎通過代碼各種形式交流。