《JS高程》—— 數據屬性與訪問器屬性

屬性類型:

ECMA-262第5版在定義只有內部才用的特性(attribute)時,描述了屬性(property)的各類特徵。ECMA-262定義這些特性是爲了實現JavaScript引擎用的,所以在JavaScript中不能直接訪問它們。爲了表示特性是內部值,該規範把它們放在兩對兒方括號中,例如[[Enumerable]]編程

ECMAScript中有兩種屬性:數據屬性和訪問器屬性數組

數據屬性:

數據屬性包含一個數據值的位置。在這個位置能夠讀取和寫入值。數據屬性有4個描述其行爲的特性。瀏覽器

  • [[Configurable]]

    1. 表示可否經過delete刪除屬性從而從新定義屬性(可配置的)
    2. 可否修改屬性的特性
    3. 可否把屬性修改成訪問器屬性。
    4. 默認值爲true
  • [[Enumerable]]

    1. 表示可否經過for-in循環遍歷屬性(可枚舉的)
    2. 默認值爲true
  • [[Writable]]

    1. 表示可否修改屬性的值。(可寫的)
    2. 默認值爲true
  • [[Value]]

    1. 包含這個屬性的數據值。
    2. 讀取屬性值的時候,從這個位置讀;
    3. 寫入屬性值的時候,把新值保存在這個位置。
    4. 默認值爲undefined

對於直接在對象上定義的屬性,它們的[[Configurable]][[Enumerable]][[Writable]]特性都被設置爲true,而[[Value]]特性被設置爲指定的值。例如:閉包

var person = {
        name:"Nicholas"
    };

這裏建立了一個名爲name的屬性,爲它指定的值是"Nicholas"。也就是說,[[Value]]特性將被設置爲"Nicholas",而對這個值的任何修改都將反映在這個位置。函數

要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接受三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是:confirgurableenumerablewritablevalue。設置其中的一或多個值,能夠修改對應的特性值。例如:oop

var person = {};
    Object.defineProperty(person,"name",{
        writable:false,
        value:"Nicholas"
    });
    
    console.log(person.name);            // "Nicholas"
    person.name = "Greg";
    console.log(person.name);            // "Nicholas"

建立了一個名爲name的屬性,它的值"Nicholas"是隻讀的。這個屬性的值是不可修改的,若是嘗試爲它指定新值,則在非嚴格模式下,賦值操做將被忽略;在嚴格模式下,賦值操做會致使拋出錯誤。this

相似的規則也適用與不可配製的屬性。例如:spa

var person = {};
    Object.defineProperty(person,"name",{
        configurable:false,
        value:"Nicholas"
    });
    
    console.log(person.name);        // "Nicholas"
    delete person.name;
    console.log(person.name);        // "Nicholas"

configurable設置爲false,表示不能從對象中刪除屬性。若是對這個屬性調用delete,則在非嚴格模式下什麼也不會發生,而在嚴格模式下會致使錯誤。
並且,一旦把屬性定義爲不可配置的(false),就不能再把它變回可配置了。此時,再調用Object.defineProperty()方法修改除writable以外的特性,都會致使錯誤:prototype

var person = {};
    Object.defineProperty(person,"name",{
        configurable:false,
        value:"Nicholas"
    });
    
    // 拋出錯誤    "Uncaught TypeError: Cannot redefine property: name"
    Object.defineProperty(person,"name",{
        configurable:true,
        value:"Nicholas"
    });

也就是說,能夠屢次調用Object.defineProperty()方法修改同一個屬性,但若是把configurable特性設置爲false以後,就會有限制了。


調用Object.defineProperty()方法時,若是不顯式指定,configurableenumerablewritable特性的默認值都是false

多數狀況下,可能都沒有必要利用Object.defineProperty()提供的這些高級功能。不過,理解這些概念對理解JavaScript對象卻很是有用。設計

IE8是第一個實現 Object.defineProperty()方法的瀏覽器版本。然而,這個版本的實現存在諸多限制:只能在DOM對象上使用這個方法,並且只能建立訪問器屬性。因爲實現不完全,建議不要在IE8中使用 Object.defineProperty()方法。

兩種定義方法的描述符默認狀況Test:

圖片描述


訪問器屬性

訪問器屬性不包含數據值;它們包含一對兒gettersetter函數(不過,這兩個函數都不是必需的)。

  • 在讀取訪問器屬時,會調用getter函數,這個函數負責返回有效的值;
  • 在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理數據。

訪問器屬性有4個特性:

  • [[Configurable]]

    1. 表示可否經過delete刪除屬性從而從新定義屬性
    2. 可否修改屬性的特性
    3. 或者可否把屬性修改成數據屬性
    4. 對於直接在對象上定義的屬性,這個特性的默認值爲true
  • [[Enumerable]]

    1. 表示可否經過for-in循環遍歷屬性
    2. 對於直接在對象上定義的屬性,這個特性的默認值爲true
  • [[Get]]

    1. 在讀取屬性時調用的函數
    2. 默認值爲undefined
  • [[Set]]

    1. 寫入屬性時調用的函數
    2. 默認值爲undefined

訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。

var book = {
        _year:2004,
        edition:1
    };
    Object.defineProperty(book,"year",{
        get:function () {
            return this._year;
        },
        set:function (newValue) {
            if ((newValue > 2004)) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    })
    book.year = 2005;
    alert(book.edition);        // 2

建立一個book對象,並給它定義兩個默認的屬性:_yearedition_year前面的下劃線是一種經常使用的記號,用於表示只能經過對象方法訪問的屬性。而訪問器屬性year則包含一個getter函數和一個setter函數

  • getter函數返回_year的值
  • setter函數經過計算來肯定正確的版本

所以,把year屬性修改成2005會致使_year變成2005,而edition變爲2。這是使用訪問器屬性的常見方式,即設置一個屬性的值會致使其餘屬性發生變化。
不必定非要同時指定getter和setter。只指定getter意味着屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定了getter函數的的屬性會拋出錯誤。相似地,沒有指定setter函數的屬性也不能讀,不然在非嚴格模式下會返回undefined,在嚴格模式下會拋出錯誤。

// 下劃線表示只能經過對象方法訪問的屬性
    var obj = {
        _x:"obj._x",
        _y:"obj._y",
        _z:"obj._z"
    };
    // 
    Object.defineProperty(obj,"x",{            // x屬性,只讀不能寫
        get:function () {
            return this._x;
        }
    });
    console.log(obj.x);                        // "obj._x"        能夠讀取,調用obj.x實際上調用了obj._x的getter函數
    obj.x = "Rewrite x";                    // 嘗試修改x屬性
    console.log(obj.x);                        // "obj._x"     寫入失敗
    //
    Object.defineProperty(obj,"y",{            // y屬性,只寫不能讀
        set:function (newValue) {
            this._y = newValue;
        }
    });
    console.log(obj.y);                        // "undefined"    讀取失敗
    obj.y = "Rewrite obj.y";                // 嘗試修改屬性
    console.log(obj._y);                    // "Rewrite obj.y" 能夠寫入
    //
    Object.defineProperty(obj,"z",{            // z屬性可讀可寫
        get:function () {
            return this._z;
        },
        set:function (newValue) {
            this._z = newValue;
        }
    });
    console.log(obj.z);                        // "obj._z"
    obj.z = "Rewrite obj.z";                // 修改z屬性
    console.log(obj._z);                    // "Rewrite obj._z"

支持ES5這個方法的瀏覽器有IE9+(IE8部分實現)、Firefox4+、Safari5+、Opera12+和Chrome。在這個方法以前,建立訪問器屬性通常使用兩個標準方法:_defineGetter_()和_defineSetter_()。這兩個方法最初是由Firefox引入的,後來Safari三、Chrome1He Opera9.5也給出了相同的實現。使用這兩個遺留的方法,能夠像下面這樣重寫前面的例子。

var book = {
        _year:2004,
        edition:1
    };
    
    // 定義訪問器的舊有方法
    book.__defineGetter__("year",function(){
        return this._year;
    });

    book.__defineSetter__("year",function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    });
    
    book.year = 2005;
    console.log(book.edition);        // 2

在不支持Object.defineProperty()方法的瀏覽器中不能修改[[Configurable]][[Enumerable]]

定義多個屬性Object.defineProperties()方法。

var book = {};
    Object.defineProperties(book,{
        _year:{
            value:2004
        },
        edition:{
            value:1
        },
        year:{
            get:function () {
                return this._year;
            },
            set:function (newValue) {
                if (newValue > 2004) {
                    this._year = newValue;
                    this.edition += newValue - 2004;
                }
            }
        },
    });

以上代碼在book對外上定義了兩個數據屬性(_yearedition)和一個訪問器屬性(year)。最終的對象與上一節中定義的對象相同。惟一區別是這裏的屬性都是在同一時間建立的。


讀取屬性的特性Object.getOwnPropertyDescriptor()

使用ES5的Object.getOwnPropertyDescriptor()方法,能夠取得給定屬性的描述符,這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,
若是是訪問器屬性,這個對象的屬性有configurableenumerablegetset;
若是是數據屬性,這個對象的屬性有configurableenumerablewritablevalue

var book = {};
    Object.defineProperties(book,{
        _year:{
            value:2004
        },
        edition:{
            value:1
        },
        year:{
            get:function () {
                return this._year;
            },
            set:function (newValue) {
                if (newValue > 2004) {
                    this._year = newValue;
                    this.edition += newValue - 2004;
                }
            }
        }
    });
    var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
    alert(descriptor.value);                // 2004 
    alert(descriptor.configurable);            // false
    alert(typeof descriptor.get);            // "undefined"
    
    var descriptor = Object.getOwnPropertyDescriptor(book,"year");
    alert(descriptor.value);                // "undefined"
    alert(descriptor.enumerable);            // false
    alert(descriptor.get);                    // "function"

對於數據屬性_year,value等於最初的值,configurable是false,而get等與undefined。
對於訪問器屬性year,value等於undefined,enumerable是false,而get是一個指向getter函數的指針。

在JavaScript中,能夠針對任何對象,包括DOM(Document Model Object)和BOM(Brower Model Object)對象。


《JavaScript編程全解》一書對訪問器屬性的說明

Objecct.create方法的第二個參數是一個關聯數組,其鍵爲屬性名,其值爲屬性描述符(屬性對象)。
下面是個具體例子。其中屬性值是經過value屬性指定的。大部分屬性默認值是false,這個例子中會將它們顯式地指定爲true。

var obj = {x:2,y:3};
    // 與下面代碼等價
    var obj = Object.create(Object.prototype,{
        x:{
            value:2,
            writable:true,
            enumerable:true,
            configurable:true
        },
        y:{
            value:3,
            writable:true,
            enumerable:true,
            configurable:true
        }
    });

能夠經過Object.create來顯式地指定屬性的屬性。


只要將get屬性與set屬性指定爲相應的函數,就可以定義一個只可以經過訪問器gettersetter來訪問值的屬性。訪問器與value屬性是互相排斥的,也就是說,若是指定了value屬性的值,訪問器(同時包括getset)就會失效;反之,若是指定了訪問器(getset中的某一個),value屬性就會失效。

(getset)與value互斥

從內部來看,將屬性做爲右值訪問時使用的是getter函數而將屬性做爲左值進行賦值時使用的是setter函數
只要能寫出正確的getter訪問器函數,就能依以此爲基礎設計出一個不可變對象。

var obj = Object.create(Object.prototype,{
        x:{
            get:function () {
                alert("get called");
            },
            set:function () {
                alert("set called");
            }
        }
    });
    
    alert(obj.x);            
    // "get called"        當要讀取屬性時,將會調用getter函數
    // undefined         因爲getter函數沒有返回值,默認返回undefined
            
    obj.x = 1;                
    // "set called"        當要寫入屬性值時,將調用setter函數

    alert(obj.x);             
    //"get called"        驗證了setter函數並無修改屬性,依舊是原屬性
    // undefined        調用getter函數,沒有指定返回值,默認返回undefined

訪問器函數也能夠經過對象字面量來表述。下面代碼中的對象與上圖是同樣的。

var obj = {
        get x() {
            alert("get called");
        },
        set x(v) {
            alert("set called");
        }
    };

getter函數與setter函數中的this引用指向的是當前對象,不過下面這樣的代碼是沒法運行的。這是由於其中的每一個訪問器函數都會不斷調用訪問器函數。(?!)

// 沒法運行,有無限loop的致命錯誤
    var obj = Object.create(Object.prototype,{
        x:{
            get:function () {return this.x},
            set:function (v) {this.x = v}
        }
    });
    alert(obj.x);
    // 讀取obj.x,隱式調用了x屬性的getter函數,函數的this引用指向obj,則this.x等價於調用obj.x。出現循環錯誤

下面的代碼雖然可以運行,但仍是有些問題

//    使用了隱藏的屬性_x的訪問器的例子(姑且算是可以運行),x做爲訪問器屬性
    var obj = Object.create(Object.prototype,{
        x:{
            get:function () {return this._x},
            set:function (v) {return this._x = v},
            x:{writable:true}
        }
    });
    obj._x = "obj._x";
    alert(obj.x);            // obj.x
    obj.x = "Rewrite obj._x";
    alert(obj.x);            // "Rewrite obj._x"

這段代碼的問題指出在於屬性_x是能夠從外部改寫的(不符合訪問器屬性規範)
不借助與規範的更恰當的方法是經過閉包來隱藏這個變量。

function createObject() {
        var _x = 0;        //    變量名也能夠用x,不過容易產生混亂,因此這裏仍使用_x
        
        // 返回了一個定義了訪問器的對象
        return {
            get x() {return _x},
            set x(v) {_x = v}
        };
    };
    
    var obj = createObject();        // 生成對象
    alert(obj.x);                    // 讀取(在內部調用getter)
    //    0
    
    obj.x = 1;                        // 改寫(在內部調用setter)
    
    alert(obj.x);
    //    1
相關文章
相關標籤/搜索