一共有两类属性。

第一类是数据属性,我们已经学习了。到目前为止,我们都是在跟数据属性打交道。

第二类属性是访问器属性。本质上它是可以获取(getting)和设置(setting)值的函数,但从外面看起来像是个普通属性。

Getter 和 Setter

访问器属性使用“getter”和“setter”表示。在对象字面量中,它们分别用 getset 表示:

  1. let obj = {
  2. get propName() {
  3. // Getter,当访问 obj.propName 时被调用
  4. },
  5. set propName() {
  6. // Setter,当设置 obj.propName = value 的时候被调用
  7. }
  8. };

当读取属性(obj.propName)的时候,会调用 getter;当给属性赋值的时候,会调用 setter。

例如,有一个对象 user,包含两个属性 namesurname

  1. let user = {
  2. name: "John",
  3. surname: "Smith"
  4. };

现在我们想要添加一个“fullName”属性,它的值应该是“John Smith”。当然,我们不想通过复制粘贴实现(没意思),所以我们可以写个访问器函数:

  1. let user = {
  2. name: "John",
  3. surname: "Smith",
  4. get fullName() {
  5. return `${ this.name } ${ this.surname }`
  6. }
  7. };
  8. alert( user.fullName ); // John Smith

从外面看,访问器属性就像是一个普通属性。我们不必像函数一样去调用 user.fullName,就像数据属性一样访问就行了:getter 会在背后被调用。

到目前为止,fullName 只有一个 getter。如果我们尝试给 user.fullName 赋值,就会有问题。

image.png

这是因为我们没有为 fullname 添加 setter 的原因:

  1. let user = {
  2. name: "John",
  3. surname: "Smith",
  4. get fullName() {
  5. return `${this.name} ${this.surname}`;
  6. },
  7. set fullName() {
  8. [this.name, this.surname] = vlaue.split(' ');
  9. }
  10. };
  11. // 给 fullName 赋值的时候,就会调用 setter
  12. user.fullName = 'Alice Cooper';
  13. alert(user.name); // Alice
  14. alert(user.surname); // Cooper

结果,我们有了一个可读可写的访问器属性。

💡 提示:**访问器属性只能使用 get/set 访问**

一个属性只能是数据属性或访问器属性,不可能既是数据属性又是访问器属性。

属性一旦使用 get prop() 或 set prop() 定义,它就是一个访问器属性。我们使用 getter 读取,使用 setter 赋值。

有时也会看见一个属性只有 setter 或 getter 的情况。在只有 setter 的情况下,属性是不可读的;在只有 getter 的情况下,属性是不可写的。


访问器描述符

访问器属性与数据属性的描述符是不一样的。

访问器属性没有 valuewritable,而代之以 getset 函数。

因此,一个访问器属性可包含:

  • get:一个不带参数的函数,在读取属性时被调用,

  • se:带一个参数的函数,给属性赋值时被调用,

  • enumerable:跟数据属性的 enumerable 标记是一个意思,

  • configurable:跟数据属性的 configurable 标记是一个意思。

例如,我们使用 Object.defineProperty 方法创建一个访问器属性 fullName,在描述符对象中使用 getset

  1. let user = {
  2. name: "John",
  3. surname: "Smith"
  4. };
  5. Object.defineProperty(user, 'fullName', {
  6. get() {
  7. return `${ this.name } ${ this.surname }`;
  8. },
  9. set(value) {
  10. [this.name, this.surname] = value.split(' ');
  11. }
  12. });
  13. alert( user.fullName ); // John Smith
  14. for (let key in user) { alert(key); } // name, surname

需要注意的是,一个属性只能是数据属性或访问器属性,一个属性不可能既是数据属性又是访问器属性。

如果我们尝试在同一个属性描述符里写入 getvalue 的话,就会报错:

image.png

根据报错信息可知:访问器(get/set)不能跟 valuewritable 同时存在。

聪明的 getter/setter

Getters/setter 可以被用作“真实”属性值的包装器,以便获得更加强大的控制。

比如,如果我们要阻止给 user 赋值太短的 name 值,就可以在 name setter 中将值保存在另一个属性中(比如 _name):

  1. let user = {
  2. get name() {
  3. return this._name;
  4. },
  5. set name(value) {
  6. if (value.length < 4) {
  7. alert('名字太短了,最少 4 个字符');
  8. return ;
  9. }
  10. this._name = value;
  11. }
  12. };
  13. user.name = 'Pete';
  14. user.name; // "Pete"
  15. user.name = ''; // 赋值失败。弹出提示:“名字太短了,最少 4 个字符”
  16. user.name; // "Pete"

名字被存在 _name 里面了,访问是通过 getter 和 setter 进行的。

从技术上讲,外部依然能直接访问 user._name 属性。只不过以“_”开头的写法是大家约定俗成表示内部变量的方式。

用来做兼容

访问器属性的最大的好处之一就是能用来包装“普通数据”,用 getter/setter 的方式访问,还能对访问行为进行微调。

举个例子:下面有一个 user 对象,现在里面是两个数据属性 nameage

  1. function User(name, age) {
  2. this.name = name;
  3. this.age = age;
  4. }
  5. let john = new User('John', 25);
  6. alert( john.age ); // 25

现在情况有变,我们存储的 age 属性变 birthday 了。因为它更加准确和方便:

  1. function User(name, birthday) {
  2. this.name = name;
  3. this.birthday = birthday;
  4. }
  5. let john = new User("John", new Date(1992, 6, 1));

这样改了的话,怎么兼容以前使用了 age 属性的地方呢?

当然,土办法是挨个找到之前使用了 age 属性的地方,然后用 birthday 替代来解决。这要是你自己写的代码,可能改了也就改了,那如果是别人呢?这样搞的话,可能就要骂娘了。而且,提前使用 age 地方应该还是使用 age 是才是最合适的,对吧?在某些地方,我要用就是 age 不是 birthday 那还非要我转换一下嘛,麻烦的。

其实呢,就是加个 age getter 就能解决的事:

  1. function User(name, birthday) {
  2. this.name = name;
  3. this.birthday = birthday;
  4. // age 属性是根据 birthday 计算的
  5. Object.defineProperty(this, "age", {
  6. get() {
  7. let todayYear = new Date().getFullYear();
  8. return todayYear - this.birthday.getFullYear();
  9. }
  10. });
  11. }
  12. let john = new User("John", new Date(1992, 6, 1));
  13. alert( john.birthday ); // Wed Jul 01 1992 00:00:00 GMT+0800 (中国标准时间)
  14. alert( john.age ); // 26

看吧,多么完美,年龄、生日两不误。真是一个好的不能再好的变通方法。

(完)


📄 文档信息

🕘 更新时间:2020/01/09
🔗 原文链接:http://javascript.info/property-accessors