JS物件屬性描述符詳解

2020-07-16 10:05:09
在 JavaScript 中,物件的屬性也可以用一些關鍵字來修飾,用以表示當前屬性是否可寫、是否有預設值、是否可列舉等,這些關鍵字就是屬性描述符。屬性描述符是 ECMAScript 5 新增的語法,它其實就是一個內部物件,用來描述物件的屬性的特性。

屬性描述符的結構

在定義物件、定義屬性時,我們曾經介紹過屬性描述符,屬性描述符實際上就是一個物件。屬性描述符一共有 6 個,可以選擇使用。
  • value:設定屬性值,預設值為 undefined。
  • writable:設定屬性值是否可寫,預設值為 true。
  • enumerable:設定屬性是否可列舉,即是否允許使用 for/in 語句或 Object.keys() 函數遍歷存取,預設為 true。
  • configurable:設定是否可設定屬性特性,預設為 true。如果為 false,將無法刪除該屬性,不能夠修改屬性值,也不能修改屬性的屬性描述符。
  • get:取值函數,預設為 undefined。
  • set:存值函數,預設為 undefined。

範例1

下面範例演示了使用 value 讀寫屬性值的基本用法。
var obj = {};  //定義空物件
Object.defineProperty(obj, 'x', {value : 100});  //新增屬性x,值為100
console.log(Object.getOwnPropertyDescriptor(obj, 'x').value);  //返回100

範例2

下面範例演示了使用 writable 屬性禁止修改屬性 x。
var obj = {};
Object.defineProperty(obj, 'x', {
    value : 1,  //設定屬性預設值為1
    writable : false  //禁止修改屬性值
});
obj.x = 2;  //修改屬性x的值
console.log(obj.x);  //1,說明修改失敗
在正常模式下,如果 writable 為 false,重寫屬性值不會報錯,但是操作失敗,而在嚴格模式下則會丟擲異常。

範例3

enumerable 可以禁止修改屬性描述符,當其值為 false 時,value、writable、enumerable 和 configurable 禁止修改,同時禁止刪除屬性。在下面範例中,當設定屬性 x 禁止修改設定後,下面操作都是不允許的,其中 obj.x=5; 若操作失敗,則後面 4 個操作方法都將丟擲異常。
var obj = Object.defineProperty({}, 'x', {
    configurable : false  //禁止設定
});
obj.x = 5;  //試圖修改其值
console.log(obj.x);  //修改失敗,返回undefined
Object.defineProperty(obj, 'x', {value : 2});  //丟擲異常
Object.defineProperty(obj, 'x', {writable: true});  //丟擲異常
Object.defineProperty(obj, 'x', {enumerable: true});  //丟擲異常
Object.defineProperty(obj, 'x', {configurable: true});  //丟擲異常
當 configurable 為 false 時,如果把 writable=true 改為 false 是允許的。只要 writable 或 configurable 有一個為 true,則 value 也允許修改。

get 和 set 函數

除了使用點語法或中括號語法存取屬性的 value 外,還可以使用存取器,包括 set 和 get 兩個函數。其中,set() 函數可以設定 value 屬性值,而 get() 函數可以讀取 value 屬性值。

借助存取器,可以為屬性的 value 設計高階功能,如禁用部分特性、設計存取條件、利用內部變數或屬性進行資料處理等。

範例1

下面範例設計物件 obj 的 x 屬性值必須為數位。為屬性 x 定義了 get 和 set 特性,obj.x 取值時,就會呼叫 get;賦值時,就會呼叫 set。
var obj = Object.create(Object.prototype, {
    _x : {  //資料屬性
        value : 1,  //初始值
        writable : true
    },
    x : {  //存取器屬性
        get : function () {  //getter
            return this._x;  //返回_x屬性值
        },
        set : function (value) {  //setter
            if (typeof value != "number") throw new Error('請輸入數位');
            this._x = value;  //賦值
        }
    }
});
console.log(obj.x);  //1
obj.x = "2";  //丟擲異常

範例2

JavaScript 也支援一種簡寫方法。針對範例 1,通過以下方式可以快速定義屬性。
var obj = {
    _x : 1,  //定義_x屬性
    get x() { return this._x },  //定義x屬性的getter
    set x(value) {  //定義x屬性的setter
        if (typeof value != "number") throw new Error('請輸入數位');
        this._x = value;  //賦值
    }
};
console.log(obj.x);  //1
obj.x = 2;
console.log(obj.x);  //2
取值函數 get() 不能接收引數,存值函數 set() 只能接收一個引數,用於設定屬性的值。

操作屬性描述符

屬性描述符是一個內部物件,無法直接讀寫,可以通過下面幾個函數進行操作。
  • Object.getOwnPropertyDescriptor():可以讀出指定物件私有屬性的屬性描述符。
  • Object.defineProperty():通過定義屬性描述符來定義或修改一個屬性,然後返回修改後的描述符。
  • Object.defineProperties():可以同時定義多個屬性描述符。
  • Object.getOwnPropertyNames():獲取物件的所有私有屬性。
  • Object.keys():獲取物件的所有本地可列舉的屬性。
  • propertyIsEnumerable():物件實體方法,直接呼叫,判斷指定的屬性是否可列舉。

範例1

在下面範例中,定義 obj 的 x 屬性允許設定特性,然後使用 Object.getOwnPropertyDescriptor() 函數獲取物件 obj 的 x 屬性的屬性描述符。修改屬性描述符的 set 函數,重設檢測條件,允許非數值型數位賦值。
var obj = Object.create(Object.prototype, {
    _x : {  //資料屬性
        value : 1,  //初始值
        writable : true
    },
    x : {  //存取器屬性
        configurable : true,  //允許修改設定
        get : function () {  //getter
            return this._x;  //返回_x屬性值
        },
        set : function (value) {
            if (typeof value != "number") throw new Error('請輸入數位');
            this._x = value;  //賦值
         }
    }
});
var des = Object.getOwnPropertyDescriptor(obj, "x");  //獲取屬性x的屬性描述符
des.set = function (value) {  //修改屬性x的屬性描述符set函數
                              //允許非數值型的數位,也可以進行賦值
    if (typeof value != "number" && isNaN(value * 1)) throw new Error('請輸入數位');
    this._x = value;
}
obj = Object.defineProperty(obj, "x", des);
console.log(obj.x);  //1
obj.x = "2";  //把一個給數值型數位賦值給屬性x
console.log(obj.x);  //2

範例2

下面範例先定義一個擴充套件函數,使用它可以把一個物件包含的屬性以及豐富的資訊複製給另一個物件。

【實現程式碼】

function extend (toObj, fromObj) {  //擴充套件物件
    for (var property in fromObj) {  //遍歷物件屬性
        if (!fromObj.hasOwnProperty(property)) continue;  //過濾掉繼承屬性
        Object.defineProperty(  //複製完整的屬性資訊
            toObj,  //目標物件
            property,  //私有屬性
            Object.getOwnPropertyDescriptor(fromObj, property)  //獲取屬性描述符
        );
    }
    return toObj;  //返回目標物件
}

【應用程式碼】

var obj = {};  //新建物件
obj.x = 1;  //定義物件屬性
extend(obj, { get y() { return 2} })  //定義讀取器物件
console.log(obj.y);  //2

控制物件狀態

JavaScript 提供了 3 種方法,用來精確控制一個物件的讀寫狀態,防止物件被改變。
  • Object.preventExtensions:阻止為物件新增新的屬性。
  • Object.seal:阻止為物件新增新的屬性,同時也無法刪除舊屬性。等價於屬性描述符的 configurable 屬性設為 false。注意,該方法不影響修改某個屬性的值。
  • Ovbject.freeze:阻止為一個物件新增新屬性、刪除舊屬性、修改屬性值。

同時提供了 3 個對應的輔助檢查函數,簡單說明如下:
  • Object.isExtensible;檢查一個物件是否允許新增新的屬性。
  • Object.isSealed:檢查一個物件是否使用了 Object.seal 方法。
  • Object.isFrozen:檢查一個物件是否使用了 Object.freeze 方法。

範例

下面程式碼分別使用 Object.preventExtensions、Object.seal 和 Object.freeze 函數控制物件的狀態,然後再使用 Object.isExtensible、Object.isSealed 和 Object.isFrozen 函數檢測物件的狀態。
var obj1 = {};
console.log(Object.isExtensible(obj1));  //true
Object.preventExtensions(obj1);
console.log(Object.isExtensible(obj1));  //false
var obj2 = {};
console.log(Object.isSealed(obj2));  //true
Object.seal(obj2);
console.log(Object.isSealed(obj2));  //false
var obj3 = {};
console.log(Object.isFrozen(obj3));  //true
Object.freeze(obj3);
console.log(Object.isFrozen(obj3));  //false