一共有两类属性。
第一类是数据属性,我们已经学习了。到目前为止,我们都是在跟数据属性打交道。
第二类属性是访问器属性。本质上它是可以获取(getting)和设置(setting)值的函数,但从外面看起来像是个普通属性。
Getter 和 Setter
访问器属性使用“getter”和“setter”表示。在对象字面量中,它们分别用 get
和 set
表示:
let obj = {
get propName() {
// Getter,当访问 obj.propName 时被调用
},
set propName() {
// Setter,当设置 obj.propName = value 的时候被调用
}
};
当读取属性(obj.propName
)的时候,会调用 getter;当给属性赋值的时候,会调用 setter。
例如,有一个对象 user
,包含两个属性 name
和 surname
:
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
赋值,就会有问题。
这是因为我们没有为 fullname
添加 setter 的原因:
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
},
set fullName() {
[this.name, this.surname] = vlaue.split(' ');
}
};
// 给 fullName 赋值的时候,就会调用 setter
user.fullName = 'Alice Cooper';
alert(user.name); // Alice
alert(user.surname); // Cooper
结果,我们有了一个可读可写的访问器属性。
💡 提示:**访问器属性只能使用
get
/set
访问**一个属性只能是数据属性或访问器属性,不可能既是数据属性又是访问器属性。
属性一旦使用
get prop(
) 或set prop()
定义,它就是一个访问器属性。我们使用 getter 读取,使用 setter 赋值。有时也会看见一个属性只有 setter 或 getter 的情况。在只有 setter 的情况下,属性是不可读的;在只有 getter 的情况下,属性是不可写的。
访问器描述符
访问器属性与数据属性的描述符是不一样的。
访问器属性没有 value
和 writable
,而代之以 get
、set
函数。
因此,一个访问器属性可包含:
get
:一个不带参数的函数,在读取属性时被调用,se
:带一个参数的函数,给属性赋值时被调用,enumerable
:跟数据属性的enumerable
标记是一个意思,configurable
:跟数据属性的configurable
标记是一个意思。
例如,我们使用 Object.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
和 value
的话,就会报错:
根据报错信息可知:访问器(get
/set
)不能跟 value
或 writable
同时存在。
聪明的 getter/setter
Getters/setter 可以被用作“真实”属性值的包装器,以便获得更加强大的控制。
比如,如果我们要阻止给 user
赋值太短的 name
值,就可以在 name
setter 中将值保存在另一个属性中(比如 _name
):
let user = {
get name() {
return this._name;
},
set name(value) {
if (value.length < 4) {
alert('名字太短了,最少 4 个字符');
return ;
}
this._name = value;
}
};
user.name = 'Pete';
user.name; // "Pete"
user.name = ''; // 赋值失败。弹出提示:“名字太短了,最少 4 个字符”
user.name; // "Pete"
名字被存在 _name
里面了,访问是通过 getter 和 setter 进行的。
从技术上讲,外部依然能直接访问 user._name
属性。只不过以“_
”开头的写法是大家约定俗成表示内部变量的方式。
用来做兼容
访问器属性的最大的好处之一就是能用来包装“普通数据”,用 getter/setter 的方式访问,还能对访问行为进行微调。
举个例子:下面有一个 user
对象,现在里面是两个数据属性 name
和 age
:
function User(name, age) {
this.name = name;
this.age = age;
}
let john = new User('John', 25);
alert( john.age ); // 25
现在情况有变,我们存储的 age
属性变 birthday
了。因为它更加准确和方便:
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));
这样改了的话,怎么兼容以前使用了 age
属性的地方呢?
当然,土办法是挨个找到之前使用了 age
属性的地方,然后用 birthday
替代来解决。这要是你自己写的代码,可能改了也就改了,那如果是别人呢?这样搞的话,可能就要骂娘了。而且,提前使用 age
地方应该还是使用 age
是才是最合适的,对吧?在某些地方,我要用就是 age
不是 birthday
那还非要我转换一下嘛,麻烦的。
其实呢,就是加个 age
getter 就能解决的事:
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
// age 属性是根据 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 ); // Wed Jul 01 1992 00:00:00 GMT+0800 (中国标准时间)
alert( john.age ); // 26
看吧,多么完美,年龄、生日两不误。真是一个好的不能再好的变通方法。
(完)
📄 文档信息
🕘 更新时间:2020/01/09
🔗 原文链接:http://javascript.info/property-accessors