物件屬性有兩種。
第一種是資料屬性。我們已經知道如何使用它們。到目前為止我們所使用的所有屬性都是資料屬性。
第二種類型的屬性是新的東西。它是存取器屬性。它們基本上是在取得和設定值時執行的函式,但對於外部程式碼而言看起來像是常規屬性。
Getter 和 Setter
存取器屬性由「getter」和「setter」方法表示。在物件文字中,它們以 get
和 set
表示
let obj = {
get propName() {
// getter, the code executed on getting obj.propName
},
set propName(value) {
// setter, the code executed on setting obj.propName = value
}
};
當讀取 obj.propName
時 getter 會運作,當它被指定時,setter 會運作。
例如,我們有一個具有 name
和 surname
的 user
物件
let user = {
name: "John",
surname: "Smith"
};
現在我們想要新增一個 fullName
屬性,它應該是 "John Smith"
。當然,我們不希望複製貼上現有資訊,因此我們可以將它實作為存取器
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
};
alert(user.fullName); // John Smith
從外部來看,存取器屬性看起來像常規屬性。這就是存取器屬性的概念。我們不會將 user.fullName
呼叫為函式,而是正常地讀取它:getter 會在幕後執行。
到目前為止,fullName
只有 getter。如果我們嘗試指定 user.fullName=
,將會出現錯誤
let user = {
get fullName() {
return `...`;
}
};
user.fullName = "Test"; // Error (property has only a getter)
讓我們透過為 user.fullName
新增 setter 來修復它
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
},
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
};
// set fullName is executed with the given value.
user.fullName = "Alice Cooper";
alert(user.name); // Alice
alert(user.surname); // Cooper
因此,我們有一個「虛擬」屬性fullName
。它可讀可寫。
存取器描述子
存取器屬性的描述子不同於資料屬性的描述子。
對於存取器屬性,沒有value
或writable
,但有get
和set
函式。
也就是說,存取器描述子可能有
get
– 沒有參數的函式,在讀取屬性時執行set
– 有 1 個參數的函式,在設定屬性時呼叫enumerable
– 與資料屬性相同configurable
– 與資料屬性相同
例如,要使用defineProperty
建立存取器fullName
,我們可以傳遞一個有get
和set
的描述子
let user = {
name: "John",
surname: "Smith"
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(" ");
}
});
alert(user.fullName); // John Smith
for(let key in user) alert(key); // name, surname
請注意,一個屬性可以是存取器(有get/set
方法)或資料屬性(有value
),但不能同時是兩者。
如果我們嘗試在同一個描述子中提供get
和value
,會產生錯誤
// Error: Invalid property descriptor.
Object.defineProperty({}, 'prop', {
get() {
return 1
},
value: 2
});
更聰明的 getter/setter
Getter/setter 可以用作「真實」屬性值的包裝器,以更有效地控制其運作。
例如,如果我們要禁止user
名稱太短,我們可以有一個 setter name
,並將值保留在另一個屬性_name
中
let user = {
get name() {
return this._name;
},
set name(value) {
if (value.length < 4) {
alert("Name is too short, need at least 4 characters");
return;
}
this._name = value;
}
};
user.name = "Pete";
alert(user.name); // Pete
user.name = ""; // Name is too short...
因此,名稱儲存在_name
屬性中,而存取是透過 getter 和 setter 進行。
技術上來說,外部程式碼可以使用user._name
直接存取名稱。但有一個廣為人知的慣例,即以底線"_"
開頭的屬性是內部的,不應從物件外部觸碰。
用於相容性
存取器的其中一個好處是,它們允許隨時透過 getter 和 setter 取代「一般」資料屬性,並調整其行為。
想像一下,我們開始使用資料屬性name
和age
實作使用者物件
function User(name, age) {
this.name = name;
this.age = age;
}
let john = new User("John", 25);
alert( john.age ); // 25
…但遲早,事情可能會改變。我們可能會決定儲存birthday
而不是age
,因為它更精確且方便
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));
現在,如何處理仍然使用age
屬性的舊程式碼?
我們可以嘗試找出所有這些地方並修復它們,但這需要時間,而且如果這些程式碼被許多其他人使用,可能會很難做到。此外,age
是user
中不錯的功能,對吧?
保留它吧。
為age
新增 getter 就能解決問題
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
// age is calculated from the current date and birthday
Object.defineProperty(this, "age", {
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let john = new User("John", new Date(1992, 6, 1));
alert( john.birthday ); // birthday is available
alert( john.age ); // ...as well as the age
現在舊程式碼也能運作,而且我們還獲得了一個不錯的額外屬性。
留言
<code>
標籤,對於多行 - 將它們包裝在<pre>
標籤中,對於超過 10 行 - 使用沙盒 (plnkr、jsbin、codepen...)