引言
相信同學們在看見這個標題的時候就一臉懵逼了,什么?JS能常量定義?別逗我好嗎?確切的說,JS當中確實沒有常量(ES6中好像有了常量定義的關鍵字),但是深入一下我們可以發現JS很多不為人知的性質,好好利用這些性質,就會發現一個不一樣的JS世界。
一、設置對象屬性
首先,在JS當中,對象的屬性其實還含有自己的隱含性質,比如下面對象:
1 var obj = {}; 2 obj.a = 1; 3 obj.b = 2;
在這里我們定義了一個對象 obj ,並且定義了這個對象的兩個屬性 a 、 b ,我們可以修改這兩個屬性的值,可以用 delete 關鍵字刪除這兩個屬性,也可以用 for ... in ... 語句枚舉 obj 對象的所有屬性,以上的這些操作叫做對象屬性的性質,在我們平常編寫代碼的時候我們會不知不覺的默認了這些性質,把他們認作為JS應有的性質,殊不知這些性質其實是可以修改的。我通常的定義的屬性的方法,默認了屬性的性質,不過我們也可以在定義屬性的時候修改屬性的性質,比如:
1 var obj = {}; 2 obj.a = 1; 3 obj.b = 2; 4 5 //等價於 6 var obj = { 7 a: 1, 8 b: 2 9 } 10 11 //等價於 12 var obj = {}; 13 Object.defineProperty(obj, "a", { 14 value: 1, //初始值 15 writable: true, //可寫 16 configurable: true, //可配置 17 enumerable: true //可枚舉 18 }); 19 Object.defineProperty(obj, "b", { 20 value: 2, //初始值 21 writable: true, //可寫 22 configurable: true, //可配置 23 enumerable: true //可枚舉 24 });
這里涉及到了一個方法,Object.defineProperty(),該方法是ES5規范中的,該方法的作用是在對象上定義一個新屬性,或者修改對象的一個現有屬性,並對該屬性加以描述,返回這個對象,我們來看一下瀏覽器兼容性:
特性 | Firefox (Gecko) | Chrome | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
基本支持 | 4.0 (2) | 5 | 9 [1] | 11.60 | 5.1 [2] |
還是天煞的IE8,如果你的項目要求兼容IE8,那么這個方法也就不適用了,不過IE8也對該方法進行了實現,只能在DOM對象上適用,而且有一些獨特的地方,在這里就不講解了。
二、認識 Object.defineProperty() 方法
Object.defineProperty() 方法可以定義對象屬性的數據描述和存儲描述,這里我們只講數據描述符,不對存儲描述符講解,數據描述符有以下選項:
configurable
- 當且僅當該屬性的 configurable 為 true 時,該屬性
描述符
才能夠被改變,也能夠被刪除。默認為false
。enumerable
當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現在對象的枚舉屬性中。
默認為false。
value
- 該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認為
undefined
。writable
當且僅當該屬性的 writable 為 true 時,該屬性才能被
賦值運算符改變。
默認為false
。
注意,當我們用常規方法定義屬性的時候,其除 value 以外的數據描述符默認均為 true ,當我們用 Object.defineProperty() 定義屬性的時候,默認為 false。
也就是說,當我們把 writable 設置為 false 的時候,該屬性是只讀的,也就滿足了常量了性質,我們把常量封裝在CONST命名空間里面:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: false, //設置屬性只讀 5 configurable: true, 6 enumerable: true 7 }); 8 console.log(CONST.A); //1 9 CONST.A = 2; //在嚴格模式下會拋錯,在非嚴格模式下靜默失敗,修改無效。
但是這樣定義的常量不是絕對的,因為我們依然可以通過修改屬性的數據描述符來修改屬性值:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: false, 5 configurable: true, 6 enumerable: true 7 }); 8 Object.defineProperty(CONST, "A", { 9 value: 2, 10 writable: true, //恢復屬性的可寫狀態 11 configurable: true, 12 enumerable: true 13 }) 14 console.log(CONST.A); //2 15 CONST.A = 3; 16 console.log(CONST.A); //3
想要做到真正的常量,還需要將屬性設置為不可配置:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: false, //設置屬性只讀 5 configurable: false, //設置屬性不可配置 6 enumerable: true 7 }); 8 console.log(CONST.A); //1 9 CONST.A = 2; //錯誤!屬性只讀 10 Object.defineProperty(CONST, "A", { 11 value: 2, 12 writable: true, 13 configurable: true, 14 enumerable: true 15 }); //錯誤!屬性不可配置
但是如果只設置屬性為不可配置狀態,依然可以對屬性值進行修改:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: true, //設置可寫 5 configurable: false, //設置屬性不可配置 6 enumerable: true 7 }); 8 console.log(CONST.A); //1 9 CONST.A = 2; 10 console.log(CONST.A); //2
進而我們可以推斷出,configurable 描述符僅凍結屬性的描述符,不會對屬性值產生影響,也就是說該描述符會凍結 writable、configurable、enumerable 的狀態,不會對屬性值加以限制:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: false, //設置不可寫 5 configurable: false, //設置屬性不可配置 6 enumerable: false //設置不可枚舉 7 }); 8 Object.defineProperty(CONST, "A", { 9 value: 2, //該屬性本身不受 configurable 的影響,但由於屬性不可寫,受 writable 的限制 10 writable: true, //錯誤!屬性不可配置 11 configurable: true, //錯誤!屬性不可配置 12 enumerable: true //錯誤!屬性不可配置 13 });
但是 configurable 的限制有一個特例,就是 writable 可以由 true 改為 false,不能由 false 改為 true:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: true, //設置可寫 5 configurable: false, //設置屬性不可配置 6 enumerable: false //設置不可枚舉 7 }); 8 Object.defineProperty(CONST, "A", { 9 value: 2, //該屬性本身不受 configurable 的影響,由於屬性可寫,修改成功 10 writable: false, 11 configurable: false, 12 enumerable: false 13 }); 14 console.log(CONST.A); //2 15 CONST.A = 3; //錯誤!屬性只讀
可枚舉描述符用於配置屬性是否可以枚舉,也就是是否會出現在 for ... in ... 語句中:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 writable: false, 5 configurable: false, 6 enumerable: true //可枚舉 7 }); 8 Object.defineProperty(CONST, "B", { 9 value: 2, 10 writable: false, 11 configurable: false, 12 enumerable: false //不可枚舉 13 }); 14 for (var key in CONST) { 15 console.log(CONST[key]); //1 16 };
有了以上的基礎,我們也就學會一種定義常量的方法,使用屬性的數據描述符,下次我們需要用到常量的時候,就可以定義一個 CONST 命名空間,將常量封裝在該命名空間里面,由於屬性描述符默認為 false,所以我們也可以這樣定義:
1 var CONST = {}; 2 Object.defineProperty(CONST, "A", { 3 value: 1, 4 enumerable: true 5 }); 6 Object.defineProperty(CONST, "B", { 7 value: 2, 8 enumerable: true 9 });
三、凍結對象
以上方法是從屬性的角度的去定義一組常量,不過我們還可以用另外一種方法,從對象的角度去配置一個對象包括它的所有屬性,Object.preventExtensions() 方法可以讓一個對象不可擴展,該對象無法再添加新的屬性,但是可以刪除現有屬性:
1 var CONST = {}; 2 CONST.A = 1; 3 CONST.B = 2; 4 Object.preventExtensions(CONST); 5 delete CONST.B; 6 console.log(CONST); //CONST: { A: 1} 7 CONST.C = 3; //錯誤!對象不可擴展
在該方法的基礎之上,我們可以使用 Object.seal() 來對一個對象密封,該方法會阻止對象擴展,並將該對象的所有屬性設置為不可配置,但是可寫:
1 var CONST = {}; 2 CONST.A = 1; 3 CONST.B = 2; 4 Object.seal(CONST); 5 CONST.A = 3; 6 console.log(CONST.A); //3 7 Object.defineProperty(CONST, "B", { 8 value: 2, 9 writable: true, 10 configurable: true, //錯誤!屬性不可配置 11 enumerable: false, //錯誤!屬性不可配置 12 }) 13 CONST.C = 3; //錯誤!對象不可擴展
也就是說 Object.seal() 方法相當於幫助我們批量的將屬性的可配置描述符設置為 false ,所以說在代碼實現層面相當於:
1 Object.seal = function (obj) { 2 Object.preventExtensions(obj); 3 for (var key in obj) { 4 Object.defineProperty(obj, key, { 5 value: obj[key], 6 writable: true, 7 configurable: false, 8 enumerable: true 9 }) 10 }; 11 return obj; 12 }
在以上兩個方法基礎上,我們可以 Object.freeze() 來對一個對象進行凍結,實現常量的需求,該方法會阻止對象擴展,並凍結對象,將其所有屬性設置為只讀和不可配置:
1 var CONST = {}; 2 CONST.A = 1; 3 CONST.B = 2; 4 Object.freeze(CONST); 5 CONST.A = 3; //錯誤!屬性只讀 6 Object.defineProperty(CONST, "B", { 7 value: 3, //錯誤!屬性只讀 8 writable: true, //錯誤!屬性不可配置 9 configurable: true, //錯誤!屬性不可配置 10 enumerable: false, //錯誤!屬性不可配置 11 }) 12 CONST.C = 3; //錯誤!對象不可擴展
從代碼實現層面上相當於:
1 Object.freeze = function (obj) { 2 Object.preventExtensions(obj); 3 for (var key in obj) { 4 Object.defineProperty(obj, key, { 5 value: obj[key], 6 writable: false, 7 configurable: false, 8 enumerable: true 9 }) 10 }; 11 return obj; 12 }
最后我們在來看一下這三個方法的兼容性:
Object.preventExtensions()
Feature | Firefox (Gecko) | Chrome | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support | 4 (2.0) | 6 | 9 | 未實現 | 5.1 |
Object.seal()
Feature | Firefox (Gecko) | Chrome | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support | 4 (2.0) | 6 | 9 | 未實現 | 5.1 |
Object.freeze()
Feature | Firefox (Gecko) | Chrome | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support | 4.0 (2) | 6 | 9 | 12 | 5.1 |
到底還是萬惡的IE,均不兼容IE8
總結
現在,我們也就有了兩種方法在JS中定義常量,第一種方法是從屬性層面上來實現,在命名空間上可以繼續添加多個常量,而第二種方法是從對象層面上來實現,對凍結對象所有屬性以及對象本身:
1 //第一種方法:屬性層面,對象可擴展 2 var CONST = {}; 3 Object.defineProperty(CONST, "A", { 4 value: 1, 5 enumerable: true 6 }); 7 8 //第二種方法:對象層面,對象不可擴展 9 var CONST = {}; 10 CONST.A = 1; 11 Object.freeze(CONST);
關於JS常量的問題就講到這里了,許多書籍在介紹JS基礎的時候都會提到JS當中沒有常量,導致許多JS開發者在一開始就默認了JS是沒有常量的這一說法。從嚴格語法意義上來講,JS確實是沒有常量的,但是我們可以通過對知識的深入和創造力來構建我們自己的常量,知識是死的,人是活的,只要我們不停的探索,滿懷着創造力,就會發現其中不一樣的世界。