有兩種類型的對象屬性。
第壹種是 數據屬性。我們已經知道如何使用它們了。到目前爲止,我們使用過的所有屬性都是數據屬性。
第二種類型的屬性是新東西。它是 訪問器屬性(accessor property)。它們本質上是用于獲取和設置值的函數,但從外部代碼來看就像常規屬性。
訪問器屬性由 “getter” 和 “setter” 方法表示。在對象字面量中,它們用 get
和 set
表示:
let obj = { get propName() { // 當讀取 obj.propName 時,getter 起作用 }, set propName(value) { // 當執行 obj.propName = value 操作時,setter 起作用 } };
當讀取 obj.propName
時,getter 起作用,當 obj.propName
被賦值時,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(屬性只有壹個 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 將以給定值執行 user.fullName = "Alice Cooper"; alert(user.name); // Alice alert(user.surname); // Cooper
現在,我們就有壹個“虛擬”屬性。它是可讀且可寫的。
訪問器屬性的描述符與數據屬性的不同。
對于訪問器屬性,沒有 value
和 writable
,但是有 get
和 set
函數。
所以訪問器描述符可能有:
get
—— 壹個沒有參數的函數,在讀取屬性時工作,
set
—— 帶有壹個參數的函數,當屬性被設置時調用,
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 可以用作“真實”屬性值的包裝器,以便對它們進行更多的控制。
例如,如果我們想禁止太短的 user
的 name,我們可以創建壹個 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 太短了……
所以,name 被存儲在 _name
屬性中,並通過 getter 和 setter 進行訪問。
從技術上講,外部代碼可以使用 user._name
直接訪問 name。但是,這兒有壹個衆所周知的約定,即以下劃線 "_"
開頭的屬性是內部屬性,不應該從對象外部進行訪問。
訪問器的壹大用途是,它們允許隨時通過使用 getter 和 setter 替換“正常的”數據屬性,來控制和調整這些屬性的行爲。
想象壹下,我們開始使用數據屬性 name
和 age
來實現 user 對象:
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
屬性的舊代碼呢?
我們可以嘗試找到所有這些地方並修改它們,但這會花費很多時間,而且如果其他很多人都在使用該代碼,那麽可能很難完成所有修改。而且,user
中有 age
是壹件好事,對吧?
那我們就把它保留下來吧。
爲 age
添加壹個 getter 來解決這個問題:
function User(name, birthday) { this.name = name; this.birthday = 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 是可訪問的 alert( john.age ); // ……age 也是可訪問的
現在舊的代碼也可以工作,而且我們還擁有了壹個不錯的附加屬性。