JavaScript 常量定義詳解

DevilShow發表於2016-12-31

相信同學們在看見這個標題的時候就一臉懵逼了,什麼?JS能常量定義?別逗我好嗎?確切的說,JS當中確實沒有常量(ES6中好像有了常量定義的關鍵字),但是深入一下我們可以發現JS很多不為人知的性質,好好利用這些性質,就會發現一個不一樣的JS世界。

首先,在JS當中,物件的屬性其實還含有自己的隱含性質,比如下面物件:

var obj = {};
obj.a = 1;
obj.b = 2;

在這裡我們定義了一個物件 obj ,並且定義了這個物件的兩個屬性 a 、 b ,我們可以修改這兩個屬性的值,可以用 delete 關鍵字刪除這兩個屬性,也可以用 for … in … 語句列舉 obj 物件的所有屬性,以上的這些操作叫做物件屬性的性質,在我們平常編寫程式碼的時候我們會不知不覺的預設了這些性質,把他們認作為JS應有的性質,殊不知這些性質其實是可以修改的。我通常的定義的屬性的方法,預設了屬性的性質,不過我們也可以在定義屬性的時候修改屬性的性質,比如:

var obj = {};
obj.a = 1;
obj.b = 2;

//等價於
var obj = {
    a: 1,
    b: 2
}

//等價於
var obj = {};
Object.defineProperty(obj, "a", {
    value: 1,              //初始值
    writable: true,        //可寫
    configurable: true,    //可配置
    enumerable: true       //可列舉
});
Object.defineProperty(obj, "b", {
    value: 2,              //初始值
    writable: true,        //可寫
    configurable: true,    //可配置
    enumerable: true       //可列舉
});

這裡涉及到了一個方法,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() 方法可以定義物件屬性的資料描述和儲存描述,這裡我們只講資料描述符,不對儲存描述符講解,資料描述符有以下選項:

configurable
當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,也能夠被刪除。預設為 false
enumerable
當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現在物件的列舉屬性中。預設為 false。
value
該屬性對應的值。可以是任何有效的 JavaScript 值(數值,物件,函式等)。預設為 undefined
writable
當且僅當該屬性的 writable 為 true 時,該屬性才能被賦值運算子改變。預設為 false

注意,當我們用常規方法定義屬性的時候,其除 value 以外的資料描述符預設均為 true ,當我們用 Object.defineProperty() 定義屬性的時候,預設為 false。

也就是說,當我們把 writable 設定為 false 的時候,該屬性是隻讀的,也就滿足了常量了性質,我們把常量封裝在CONST名稱空間裡面:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: false, //設定屬性只讀
    configurable: true,
    enumerable: true
});
console.log(CONST.A);  //1
CONST.A = 2; //在嚴格模式下會拋錯,在非嚴格模式下靜默失敗,修改無效。

但是這樣定義的常量不是絕對的,因為我們依然可以通過修改屬性的資料描述符來修改屬性值:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: false,
    configurable: true,
    enumerable: true
});
Object.defineProperty(CONST, "A", {
    value: 2,
    writable: true,  //恢復屬性的可寫狀態
    configurable: true,
    enumerable: true
})
console.log(CONST.A);  //2
CONST.A = 3;
console.log(CONST.A);  //3

想要做到真正的常量,還需要將屬性設定為不可配置:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: false,        //設定屬性只讀
    configurable: false,    //設定屬性不可配置
    enumerable: true
});
console.log(CONST.A);  //1
CONST.A = 2;  //錯誤!屬性只讀
Object.defineProperty(CONST, "A", {
    value: 2,
    writable: true, 
    configurable: true,
    enumerable: true
});  //錯誤!屬性不可配置

但是如果只設定屬性為不可配置狀態,依然可以對屬性值進行修改:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: true,         //設定可寫
    configurable: false,    //設定屬性不可配置
    enumerable: true
});
console.log(CONST.A);  //1
CONST.A = 2;
console.log(CONST.A);  //2

進而我們可以推斷出,configurable 描述符僅凍結屬性的描述符,不會對屬性值產生影響,也就是說該描述符會凍結 writable、configurable、enumerable 的狀態,不會對屬性值加以限制:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: false,         //設定不可寫
    configurable: false,     //設定屬性不可配置
    enumerable: false        //設定不可列舉
});
Object.defineProperty(CONST, "A", {
    value: 2,                //該屬性本身不受 configurable 的影響,但由於屬性不可寫,受 writable 的限制
    writable: true,          //錯誤!屬性不可配置
    configurable: true,      //錯誤!屬性不可配置
    enumerable: true         //錯誤!屬性不可配置
});

但是 configurable 的限制有一個特例,就是 writable 可以由 true 改為 false,不能由 false 改為 true:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: true,          //設定可寫
    configurable: false,     //設定屬性不可配置
    enumerable: false        //設定不可列舉
});
Object.defineProperty(CONST, "A", {
    value: 2,  //該屬性本身不受 configurable 的影響,由於屬性可寫,修改成功
    writable: false, 
    configurable: false, 
    enumerable: false 
});
console.log(CONST.A);  //2
CONST.A = 3;  //錯誤!屬性只讀

可列舉描述符用於配置屬性是否可以列舉,也就是是否會出現在 for … in … 語句中:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    writable: false,
    configurable: false,
    enumerable: true  //可列舉
});
Object.defineProperty(CONST, "B", {
    value: 2,
    writable: false,
    configurable: false,
    enumerable: false  //不可列舉
});
for (var key in CONST) {
    console.log(CONST[key]);  //1
};

有了以上的基礎,我們也就學會一種定義常量的方法,使用屬性的資料描述符,下次我們需要用到常量的時候,就可以定義一個 CONST 名稱空間,將常量封裝在該名稱空間裡面,由於屬性描述符預設為 false,所以我們也可以這樣定義:

var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    enumerable: true
});
Object.defineProperty(CONST, "B", {
    value: 2,
    enumerable: true
});

以上方法是從屬性的角度的去定義一組常量,不過我們還可以用另外一種方法,從物件的角度去配置一個物件包括它的所有屬性,Object.preventExtensions() 方法可以讓一個物件不可擴充套件,該物件無法再新增新的屬性,但是可以刪除現有屬性:

var CONST = {};
CONST.A = 1;
CONST.B = 2;
Object.preventExtensions(CONST);
delete CONST.B;
console.log(CONST);  //CONST: { A: 1}
CONST.C = 3;  //錯誤!物件不可擴充套件

在該方法的基礎之上,我們可以使用 Object.seal() 來對一個物件密封,該方法會阻止物件擴充套件,並將該物件的所有屬性設定為不可配置,但是可寫:

var CONST = {};
CONST.A = 1;
CONST.B = 2;
Object.seal(CONST);
CONST.A = 3;
console.log(CONST.A);  //3
Object.defineProperty(CONST, "B", {
    value: 2,
    writable: true,       
    configurable: true,  //錯誤!屬性不可配置
    enumerable: false,   //錯誤!屬性不可配置
})    
CONST.C = 3;  //錯誤!物件不可擴充套件

也就是說 Object.seal() 方法相當於幫助我們批量的將屬性的可配置描述符設定為 false ,所以說在程式碼實現層面相當於:

Object.seal = function (obj) {
    Object.preventExtensions(obj);
    for (var key in obj) {
        Object.defineProperty(obj, key, {
            value: obj[key],
            writable: true,
            configurable: false,
            enumerable: true
        })
    };
    return obj;
}

在以上兩個方法基礎上,我們可以 Object.freeze() 來對一個物件進行凍結,實現常量的需求,該方法會阻止物件擴充套件,並凍結物件,將其所有屬性設定為只讀和不可配置:

var CONST = {};
CONST.A = 1;
CONST.B = 2;
Object.freeze(CONST);
CONST.A = 3;  //錯誤!屬性只讀
Object.defineProperty(CONST, "B", {
    value: 3,            //錯誤!屬性只讀
    writable: true,      //錯誤!屬性不可配置
    configurable: true,  //錯誤!屬性不可配置
    enumerable: false,   //錯誤!屬性不可配置
})    
CONST.C = 3;  //錯誤!物件不可擴充套件

從程式碼實現層面上相當於:

Object.freeze = function (obj) {
    Object.preventExtensions(obj);
    for (var key in obj) {
        Object.defineProperty(obj, key, {
            value: obj[key],
            writable: false,
            configurable: false,
            enumerable: true
        })
    };
    return obj;
}

最後我們在來看一下這三個方法的相容性:

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中定義常量,第一種方法是從屬性層面上來實現,在名稱空間上可以繼續新增多個常量,而第二種方法是從物件層面上來實現,對凍結物件所有屬性以及物件本身:

//第一種方法:屬性層面,物件可擴充套件
var CONST = {};
Object.defineProperty(CONST, "A", {
    value: 1,
    enumerable: true
});

//第二種方法:物件層面,物件不可擴充套件
var CONST = {};
CONST.A = 1;
Object.freeze(CONST);

關於JS常量的問題就講到這裡了,許多書籍在介紹JS基礎的時候都會提到JS當中沒有常量,導致許多JS開發者在一開始就預設了JS是沒有常量的這一說法。從嚴格語法意義上來講,JS確實是沒有常量的,但是我們可以通過對知識的深入和創造力來構建我們自己的常量,知識是死的,人是活的,只要我們不停的探索,滿懷著創造力,就會發現其中不一樣的世界。

相關文章