2022 年 4 月 13 日

屬性 getter 和 setter

物件屬性有兩種。

第一種是資料屬性。我們已經知道如何使用它們。到目前為止我們所使用的所有屬性都是資料屬性。

第二種類型的屬性是新的東西。它是存取器屬性。它們基本上是在取得和設定值時執行的函式,但對於外部程式碼而言看起來像是常規屬性。

Getter 和 Setter

存取器屬性由「getter」和「setter」方法表示。在物件文字中,它們以 getset 表示

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 會運作。

例如,我們有一個具有 namesurnameuser 物件

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。它可讀可寫。

存取器描述子

存取器屬性的描述子不同於資料屬性的描述子。

對於存取器屬性,沒有valuewritable,但有getset函式。

也就是說,存取器描述子可能有

  • get – 沒有參數的函式,在讀取屬性時執行
  • set – 有 1 個參數的函式,在設定屬性時呼叫
  • enumerable – 與資料屬性相同
  • configurable – 與資料屬性相同

例如,要使用defineProperty建立存取器fullName,我們可以傳遞一個有getset的描述子

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),但不能同時是兩者。

如果我們嘗試在同一個描述子中提供getvalue,會產生錯誤

// 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 取代「一般」資料屬性,並調整其行為。

想像一下,我們開始使用資料屬性nameage實作使用者物件

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屬性的舊程式碼?

我們可以嘗試找出所有這些地方並修復它們,但這需要時間,而且如果這些程式碼被許多其他人使用,可能會很難做到。此外,ageuser中不錯的功能,對吧?

保留它吧。

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

現在舊程式碼也能運作,而且我們還獲得了一個不錯的額外屬性。

教學課程地圖

留言

在留言前請先閱讀...
  • 如果您有改善建議 - 請 提交 GitHub 問題 或發起拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容 - 請詳細說明。
  • 要插入一些程式碼,請使用 <code> 標籤,對於多行 - 將它們包裝在 <pre> 標籤中,對於超過 10 行 - 使用沙盒 (plnkrjsbincodepen...)