一共有两类属性。
第一类是数据属性,我们已经学习了。到目前为止,我们都是在跟数据属性打交道。
第二类属性是访问器属性。本质上它是可以获取(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 赋值的时候,就会调用 setteruser.fullName = 'Alice Cooper';alert(user.name); // Alicealert(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 Smithfor (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
