JavaScript物件的資料屬性與訪問器屬性

wrfan發表於2019-04-22

首先介紹了JavaScript物件資料屬性與訪問器屬性的相關概念,然後介紹了屬性定義與讀取的相關方法,最後對JavaScript物件資料屬性與訪問器屬性的知識做了一些擴充套件,並手動實現了一個簡單的資料雙向繫結例項。

文中如果有疏漏錯誤之處,各位小夥伴可在文末評論中提出,fanweiren一定及時修正,以避免誤導。另外,如果對本文有不理解的地方,也歡迎評論提出,fanweiren一定積極為大家解答。

一. 建立JavaScript物件

建立JavaScript對簡單的方法有兩種:一是直接建立一個object例項,然後為他新增屬性與方法,二是通過物件字面量語法的方式建立。

1.1 new object

var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
    alert(this.name);
};
複製程式碼

上述例項建立了一個名為person的例項物件,併為其新增了三個屬性(nameagejob)和一個方法(sayName)。

1.2 物件字面量

上述示例同樣可通過物件字面量的語法實現如下。

var person = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function() {
        alert(this.name);
    }
};
複製程式碼

二. 屬性型別

對於1.11.2的建立的每個例項都有自己的屬性和方法,這些屬性在建立時都帶有一些特徵值,這些特徵值僅供內部使用,在JavaScript中不能直接訪問他們,按規定這些特徵值需要放在兩對方括號中,例如[[Enumerable]]

ECMAScript中有兩類屬性,分別是資料屬性選擇器屬性

1.1 資料屬性

資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值。資料屬性有 4 個描述其行為的 特性。

  • [[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。
  • [[Enumerable]]:表示能否通過for-in迴圈返回屬性。
  • [[Writable]]:表示能否修改屬性的值。
  • [[Value]]:包含這個屬性的資料值。

通過new object或者物件字面量方式建立物件定義的屬性,他們的[[Configurable]][[Enumerable]][[Writable]]特徵均為true,而[[Value]]值被設定為指定的值。

var person = {
    name: "Nicholas"
}
複製程式碼

以上程式碼通過物件字面量的方式定義了一個person物件,併為它指定了name屬性,此時name屬性的四個特徵值,[[Configurable]][[Enumerable]][[Writable]]特徵均為true,而[[Value]]Nicholas

要修改屬性預設的特性,必須使用Object.defineProperty()方法,可同時修改一個或多個特性值,該方法接收三個引數:

  • object:屬性所在的物件
  • propertyName:屬性的名字
  • descripor:屬性的描述符,必須是[[Configurable]][[Enumerable]][[Writable]][[Value]]中的一個或者多個值。
var person = {
    name: fanweiren
}; 
Object.defineProperty(person, "name", { 
 writable: false, 
 value: "Nicholas" 
}); 
alert(person.name); //"Nicholas" 
person.name = "Greg"; 
alert(person.name); //"Nicholas"
複製程式碼

以上程式碼將name屬性的值設定為設定為Nicholas,並將name屬性的[[writable]]特性值設為false,表示該屬性只可讀不可修改。如果嘗試為它指定新值,非嚴格模式下,賦值操作會被忽略,在嚴格模式下,會丟擲錯誤。

var person = {
    name: fanweiren
}; 
Object.defineProperty(person, "name", { 
 configurable: false, 
 value: "Nicholas" 
}); 
alert(person.name); //"Nicholas" 
delete person.name; 
alert(person.name); //"Nicholas"
複製程式碼

[[Configurable]]特性設定為false後,屬性name既不能從物件中刪除,此時呼叫delete在非嚴格模式下什麼也不會發生,在嚴格模式下會丟擲錯誤。

一旦把[[Configurable]]特性設定為false即不可配置後,就不能在變回可配置了。且設定為false後,[[Enumerable]][[Writable]]也都不能修改了。

發現一個奇怪的現象,[[Configurable]]特性設定為false後,如果[[Writable]]以前為true,則還可以將其修改為false,如果[[Writable]]以前為false,則不可修改。

1.2 訪問器屬性

訪問器屬性不包含資料值;它們包含一對兒gettersetter 函式(不過,這兩個函式都不是必需的)。在讀取訪問器屬性時,會呼叫getter 函式,這個函式負責返回有效的值;在寫入訪問器屬性時,會呼叫 setter函式並傳入新值,這個函式負責決定如何處理資料。訪問器屬性有如下 4 個特性。

  • [[Configurable]]:特性同1.1資料屬性的該特徵值
  • [[Enumerable]]:特性同1.1資料屬性的該特徵值
  • [[Get]]:在讀取屬性時呼叫的函式。預設值為 undefined。
  • [[Set]]:在寫入屬性時呼叫的函式。預設值為 undefined。

訪問器屬性不能直接定義,必須使用 Object.defineProperty()來定義。請看下面的例子。

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

以上程式碼定義了hideYearedition兩個資料屬性,同時定義了year這個訪問器屬性。year訪問器屬性包含getset兩個方法,get方法返回hideYear屬性的值,set方法通過計算來確定正確的版本。

get和set可以不同時使用,但在嚴格模式下只其中一個,會丟擲錯誤

三點總結:

  1. 所有直接通過new object或者物件字面量為物件新增的屬性均為資料屬性,訪問器屬性必須用Object.defineProperty()定義。
  2. 通過new object或者物件字面量為物件新增的屬性,其[[Configurable]][[Enumerable]][[Writable]]屬性預設均為true,通過Object.defineProperty()定義的屬性,對於defineProperty方法沒定義的特徵,預設為false
// 通過`new object`或者物件字面量為物件新增的屬性
var person = {
    name: 'fanweiren'
} // configurable、enumerable、writable均為true

// 通過`Object.defineProperty()`定義的屬性
Object.defineProperty(book, "name", {
    value: 'fanweiren'
} // configurable、enumerable、writable均為false

Object.defineProperty(book, "name", {
    configurable: true
    value: 'fanweiren'
}// configurable為true,enumerable、writable為false
複製程式碼
  1. 一個屬性要麼為資料屬性,要麼為訪問器屬性,不可能既是資料屬性又是訪問器屬性,即資料描述符與存取描述符不可混用,會丟擲錯誤。
var obj = {};
Object.defineProperty(obj, 'a', {
    value: 'a1',
    get: function() {
       return 'a2'
    } // 錯誤!資料描述符與存取描述符不可混用
});
複製程式碼
  1. 訪問器屬性使用的常見方式,即設定一個值會導致其他屬性發生變化,該思想是vue雙向繫結資料實現的核心內涵。

三. 定義多個屬性

利用Object.defineProperty()定義單個屬性。利用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()方法可以取得給定屬性的描述符。這個方法返回值是一個物件,如果是訪問器屬性,這個物件的屬性有configurableenumerablegetset;如果是資料屬性,這 個物件的屬性有configurableenumerablewritablevalue。對於第三部分(定義多個屬性)的程式碼,呼叫Object.getOwnPropertyDescriptor()方法後結果如下:

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(JSON.stringify(descriptor))
//{"value":2004,"writable":false,"enumerable":false,"configurable":false}
alert(typeof descriptor.get);     //"undefined"

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(JSON.stringify(descriptor)) // {"enumerable":false,"configurable":false}
alert(typeof descriptor.get); // function
alert(typeof descriptor.set); // function

複製程式碼

五 相關擴充套件

5.1 全域性環境中的賦值導致configurable預設為false

全域性環境下,a=1中的a相當於window的一個屬性,而var a=1中的a相當於定義的一個變數。

a = 1; //a是其所在物件的一個屬性, configurable與物件字面量定義屬性一樣,預設為true
複製程式碼
var a = 1; // 通過var或let初始化的,configurable屬性預設為false
複製程式碼

5.2. 利用訪問器屬性實現簡單的資料雙向繫結

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<input id="input"/></br>
<button id="btn">點我修改值</button>
<script>
    let inputNode = document.getElementById('input');
    let person = {}
    Object.defineProperty(person, 'name' ,{
        configurable: true,
        get: function () {
            console.log('person.name.get():'+ inputNode.value)
            return inputNode.value
        },
        set: function (newValue) {
            console.log('person.name.set():' + newValue)
            inputNode.value = newValue
        }
    })
    inputNode.oninput = function () {
        console.log('inputNode.oninput: ' + inputNode.value)
        person.name = inputNode.value;
    }

    let btn = document.getElementById('btn');
    btn.onclick = function () {
        person.name = Math.random()
    }
</script>
</body>
</html>
複製程式碼

以上程式碼實現了一個將input輸入框的資料與perosn物件的name屬性值繫結的例項,input輸入框內的資料發生改變會同步影響perosn.name的值,而對perosn.name的任何修改也會同步到input輸入框內。

注意是將inputperosn.name資料繫結,而不是inputbutton繫結,button的作用僅僅是改變perosn.name的值,方便演示而已。 具體步驟為

  1. 首先寫一個簡單HTML頁面,包括一個input輸入框與一個button按鈕
  2. perosn.name定義訪問器屬性的特性,重寫setget方法,set方法會獲取input輸入框的值並返回,而set方法會為input輸入框重新賦值。當perosn.name被賦值時,input輸入框的值會同步變化。
  3. input輸入框新增監聽方法,input輸入值發生改變時會被觸發,觸發此方法時input內的值會被賦值給perosn.name,導致perosn.name同步變化。
  4. btn按鈕新增點選的監聽方法,主要是為了方便為perosn.name賦值,以觀察第3步所說的現象。如果要觀察第2步的現象,可以按F12觀察console頁面的列印資料。

下圖為上述程式碼的示意圖。

JavaScript物件的資料屬性與訪問器屬性

六. 總結

本文從物件的建立引出物件資料屬性與訪問器屬性的概念,首先介紹了資料屬性與訪問器屬性的相關知識,然後介紹了屬性定義與讀取的相關方法。主要結論如下:

  1. javascript物件的屬性分為資料屬性與訪問器屬性兩類,通過new object與物件字面量定義的屬性都為資料屬性,訪問器屬性必須通過Object.defineProperty()方法定義。
  2. 通過new object或者物件字面量為物件新增的屬性,其[[Configurable]][[Enumerable]][[Writable]]屬性預設均為true,通過Object.defineProperty()定義的屬性,對於defineProperty方法沒定義的特徵,預設為false
  3. 一個屬性要麼為資料屬性,要麼為訪問器屬性,不可能既是資料屬性又是訪問器屬性。
  4. 訪問器屬性使用的常見方式,即設定一個值會導致其他屬性發生變化,該思想是vue雙向繫結資料實現的核心內涵。
  5. [[Configurable]]表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性,[[Enumerable]]表示能否通過for-in迴圈返回屬性。

相關文章