本资料仅提供以下语言版本:English, 日本語。请 帮助我们 将其翻译为 简体中文 版本。

属性的 getter 和 setter

有两种类型的属性

第一种是数据属性。我们已经知道如何使用它们。实际上,我们迄今为止使用的所有属性都是数据属性。

第二种类型的属性是新东西。它是 访问器属性(accessor properties)。它们本质上是获取和设置值的函数,但从外部代码来看像常规属性。

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。

例如,我们有一个具有 namesurname 属性的 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=,将会出现错误。

我们通过为 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

现在我们有一个“虚拟”属性。它是可读写的,但实际上并不存在。

访问器属性只能访问 get/set

属性可以是“数据属性”或“访问器属性”,但不能同时属于两者。

一旦使用 get prop()set prop() 定义了一个属性,它就是一个访问器属性。所以必须有一个getter来读取它,如果我们对它赋值,它必须是一个 setter。

有时候只有一个 setter 或者只有一个 getter 是正常的。但在这种情况下,该属性将不可读或可写。

访问器描述符

访问器属性的描述符与数据属性相比是不同的。

对于访问器属性,没有 valuewritable,但是有 getset 函数。

所以访问器描述符可能有:

  • get —— 一个没有参数的函数,在读取属性时工作,
  • set —— 带有一个参数的函数,当属性被设置时调用,
  • 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

请再次注意,属性可以要么是访问器,要么是数据属性,而不能两者都是。

如果我们试图在同一个描述符中提供 getvalue,则会出现错误:

// Error: Invalid property descriptor.
Object.defineProperty({}, 'prop', {
  get() {
    return 1
  },

  value: 2
});

更聪明的 getter/setters

Getter/setter 可以用作“真实”属性值的包装器,以便对它们进行更多的控制。

例如,如果我们想禁止为 user 设置太短的名称,我们可以将 name 存储在一个特殊的 _name 属性中。并在 setter 中过滤赋值操作:

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...

从技术上讲,外部代码仍然可以通过使用 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 属性的旧代码?

我们可以尝试找到所有这些地方并修复它们,但这需要时间,而且如果该代码是由其他人编写的,则很难做到。另外,age 放在 user 中也是一件好事,对吧?在某些地方,这正是我们想要的。

age 添加 getter 可缓解问题:

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // age 是由当前日期和生日计算出来的
  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 也是可访问的

现在旧的代码也可以工作,而且我们拥有了一个很好的附加属性。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。