王國維在《人間詞話》裡談到了治學經驗,他說:“古今之成大事業、大學問者,必經過三種之境界。”
巧合的是,最近受 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(o);複製程式碼
總結下就是設定:
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倉庫,歡迎通過程式碼各種形式交流。