JS 对象中的属性(prop)背后是由一个属性描述符(descriptor)来描述当前属性的行为特性,如是否可被枚举、是否可被重新赋值…
属性描述符可简称描述符,并有两种类型:数据描述符和存取描述符(也称访问器描述符)。这两种描述符也都是对象,它们有以下键值:
两种描述符共享的键值
-
configurable
- 默认为
false
- 配置对象属性的描述符是否可被改变、属性是否可被删除
- 默认为
-
enumerable
- 默认为
false
- 配置对象属性是否可被枚举
- 若为
true
,则该属性可被枚举,可出现在诸如for...in...
、Object.keys()
、Object.entries()
等迭代遍历操作中
- 默认为
数据描述符独有的键值
-
value
- 默认为
undefined
- 配置对象属性的值,可以是字符串、对象、函数…
- 默认为
-
writable
- 默认为
false
- 配置对象属性是否可被赋值运算符改变值
- 默认为
存取描述符独有的键值
-
get
- 默认为
undefined
- 配置对象属性的
getter
函数——一个没有参数的函数(但是会传入this
),在读取属性时自动调用,读取属性的结果为函数的返回值
- 默认为
-
set
- 默认为
undefined
- 配置对象属性的
setter
函数——一个带有一个参数的函数,在属性被赋值时自动调用,要赋的新值即为函数的参数
- 默认为
描述符默认值汇总
总的来说,描述符的默认值都比较保守。
enumerable
configurable
writable
默认值都是false
value
get
set
默认值都是undefined
查看属性的描述符
- 通过
Object.getOwnPropertyDescriptor(obj, prop)
可以查看对象单个属性的描述符 - 通过
Object.getOwnPropertyDescriptors(obj)
可以查看对象所有属性的描述符
类似于: let obj = {name: 'John'}
、obj.age = 18
,通过这种常用的方式创建的出来的属性都是数据属性,且描述符 configurable
writable
enumrable
的值都为 true
。
let obj = { name: 'John' }
Object.getOwnPropertyDescriptor(obj, 'name')
// -> {value: 'John', configurable: true, writable: true, enumerable: true}
obj.age = 18
Object.getOwnPropertyDescriptors(obj)
/* ->
{
name: {
value: 'Jhon',
configurable: true,
writable: true,
enumerable: true
},
age: {
value: 18,
configurable: true,
writable: true,
enumerable: true
}
}
*/
定义属性的描述符
- 通过
Object.defineProperty(obj, prop, descriptor)
可以为对象的某个属性设置描述符 - 通过
Object.defineProperties(obj, descriptors)
可以一次为对象设置多个属性描述符
let obj = {}
Object.defineProperty(obj, 'name', {
value: 'John',
writable: false // 不可写
})
obj.name = 'Frank' // 报错!赋值运算失败
let descriptor = Object.getOwnPropertyDescriptor(obj, 'name')
descriptor.enumerable // -> false; 没有在 defineProperty() 中指明的配置项,均默认为 false
descriptor.configurable // -> false; 没有在 defineProperty() 中指明的配置项,均默认为 false
delete obj.name // 执行失败!因为 name 属性的描述符 configurable 为 false
Object.defineProperties(obj, {
age: { value: 18; enumerable: true }, // 可枚举
surname: { value: 'Smith', enumerable: false } // 不可枚举
})
Object.keys(obj) // -> ['age']
访问器属性由 “getter” 和 “setter” 方法表示。在对象字面量中,它们用
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(' ')
},
})
user.fullName // -> 'John Smith'
user.fullName = 'Alice Cooper'
Object.values(user) // -> ['Alice', 'Cooper']
如果存取描述符只声明了 get()
方法,那么该对象属性可读不可写;如果只声明了 set()
方法,那么该对象属性可写不可读;只有同时声明了 get()
方法和 set()
方法,该属性才可读可写。
存取属性和数据属性
如果对象属性的描述符是存取描述符,那么该属性也称为存取属性;同理,如果对象属性的描述符为数据描述符,那么该属性也称为数据属性。
存取属性有别于数据属性,我们可以将其视为一种“虚拟”的数据属性:
let user = {
// 声明数据属性 name
name: 'John',
// 声明数据属性 surname
surname: 'Smith',
// 声明访问器属性 fullName
get fullName() {
return `${this.name} ${this.surname}`
},
set fullName(value) {
;[this.name, this.surname] = value.split(' ')
},
}
数据属性 name
surname
的读写是直接对该属性操作;而读取访问器属性 fullName
时,是通过执行 get()
方法获得返回值,写入访问器属性时,是通过执行 set()
方法,也正因为如此,我们可以在访问器属性上实现更丰富、更灵活的功能。
实现 get()
set()
时,请注意堆栈溢出的问题:
// 这样定义访问器属性会导致程序堆栈溢出
let obj = {
get prop() {
return this.prop
},
set prop(val) {
this.prop = val
},
}
// 为访问器属性 prop 引入中间属性 _prop 即可避免堆栈溢出
let obj = {
get prop() {
return this._prop
},
set prop(val) {
this._prop = val
},
}
上述例子中的内部变量 _prop
,在任何时候都不应在外部对其操作!
通过 set()
,在用户赋值时给予适当的提示:
let user = {
get name() {
return this._name
},
set name(value) {
if (value.length < 2) {
alert('名字太短了,请至少输入 2 个字符 🙏')
return
}
this._name = value
},
}
JS 变量改变时,重新渲染视图: