对象用来存储属性。
直到现在,属性对我们来说就是简单“键:值”对的形式。但对象属性还可以更加灵活和强大。
本节我们将引入“属性描述符”的概念,介绍了“数据属性”。而在下一章中,则会介绍另一种属性——“访问器属性”。
属性标记
对象属性,除了一个 value,还有 3 个特别的属性(或叫标记,英文单词 flags)。
writable:如果为true,表示属性值可修改;否则是只读的。enumerable:如果为true,表示属性可枚举,能被循环遍历(比如for...in);否则不能被循环遍历。configurable:如果为true,表示属性能被删除(即可以这么操作delete obj.foo),属性标记可被修改;否则既不能删除属性,也不可修改属性标记。
你可能懵逼了,心想“之前没看到过啊”。当然啦,这些可不是一般人能看到的,如果你是使用“通常的方式”创建属性的,肯定不知道原来一个属性里还有这么多东西的。当我们用上面所说的“通常的方式”创建属性的时候,这些标记的默认值都是 true,当然,这些标记值我们是可以修改的。
在此之前,先来看下如果得到这些标记。
答案是使用 Object.getOwnPropertyDescriptor 得到关于一个属性的所有信息。
此方法的语法如下:
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
propertyName表示获取的是哪个属性obj是获取属性的源对象。对应到这里,我们是要获取obj.``propertyName的属性信息。
此方法的返回值是一个对象,被称为“属性描述符”:其中包含一个 value,还有所有标记。
举个例子:
let user = {name: "John"};let descriptor = Object.getOwnPropertyDescriptor(user, 'name');alert( JSON.stringify(descriptor, null, 2 ) );/* property descriptor:{"value": "John","writable": true,"enumerable": true,"configurable": true}*/
修改属性标记,我们使用的是 Object.defineProperty。
语法如下:
Object.defineProperty(obj, propertyName, descriptor)
obj、propertyName:跟getOwnPropertyDescriptor方法参数意思一样。不过这里不是在获取属性,而是设置属性了。descriptor:这个就是描述符对象了。因为是设置属性,那么这个描述符就是用来描述obj.propertyName这个属性的。
defineProperty 方法还有个特点:如果设置的属性是已经存在的,那么这个方法做的就是更新属性标记的操作;否则,就使用给定的标识符对象创建属性。另外还需要说明的是,创建属性时,没有指明的标记,默认都当做 false 设置。
举个例子。下面的 name 属性的所有标记值都会是 false:
let user = {};Object.defineProperty(user, "name", {value: "John"});let descriptor = Object.getOwnPropertyDescriptor(user, 'name');alert( JSON.stringify(descriptor, null, 2 ) );/*{"value": "John","writable": false,"enumerable": false,"configurable": false}*/
与上面以“通常方式”创建的属性(user.name)相比,这里的不同之处在于:当前所有的标记值都为 false。因此,如果需要指定某个标记为 true,必须在 descriptor 中明确指明。
现在我们再来细看,这里的每个标记具体会影响属性哪些方面的表现。
不可写的
我们通过设置 user.name 属性的 writable 标记为 false,将该属性变成不可写的(non-writable,即不能赋值)。

现在 user 的 name 属性是修改不了了,除非再使用 defineProperty 方法去覆盖 writable 标志为 true。
💡 提示:严格模式下会报错**
虽然赋值失败了,但上面的代码并没有报错。同样的这一段代码,如果是在严格模式下执行的,就能看见报错。

下面几乎一样的代码,不过这次变为添加新属性了。
let user = { };Object.defineProperty(user, 'name', {value: 'John',// 对新属性,我们需要明确指定哪个标记为 true,// 这里没有指定 writable 标记,因此这个属性是不可写的enumerable: true,configurable: true});console.log(user.name); // "John"user.name = 'Pete';console.log(user.name); // "John"。因为 name 属性不可写,所以它的值还是 "John",而非 "Pete"
不可枚举
现在给 user 添加一个 toString 方法属性。
通常情况下,对象内置的 toString 方法是不可枚举的,不会出现在 for...in 循环里。但是如果是自己添加的 toString,就会出现在 for...in 里:
let user = {name: 'John',toString () {return this.name;}};// 默认,toString 出现在枚举列表中for (let key in user) { alert(key); } // name, toString
如果我们不喜欢,就设置 enumerable: false,toString 就不会出现在 for..in 里了:
let user = {name: 'John',toString () {return this.name;}};Object.defineProperty(user, 'toString', {enumerable: false});// 现在 toString 不会出现在循环里了for (let key in user) { alert(key); } // name
Object.keys 方法也会排除不可枚举属性:
alert( Object.keys(user) ); // name
不可配置
有些内置对象或属性就是不可配置的(configurable: false)。
不可配置属性不能被删除。
比如,内置属性 Math.PI 不仅是不可写的、不可枚举的,而且还不可配置。
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');alert( JSON.stringify(descriptor, null, 2) );/*{"value": 3.141592653589793,"writable": false,"enumerable": false,"configurable": false}*/
所以,我们不能修改 Math.PI 的值,使用 delete Math.PI 试图删除 PI 也是无效的。

一旦将某个属性设置为不可配置后,就不能在使用 Object.defineProperty 方法修改这个属性了。
更加准确地表述是,不可配置性对 Object.defineProperty 施加了如下几个限制:
- 不可修改
configurable标记。 - 不可修改
enumerable标记。 - 不可将
writable: false设置为true(反之亦然)。 - 不可修改访问器属性的
get/set(但可以添加)。
下面我们将 user.name 定义成一个“永远封存”的常量了。
let user = {};Object.defineProperty(user, 'name', {value: 'John',writable: false,configurable: false});// 我们不能修改 user.name 的值或它的标记了// 下面的操作都是无效的:// user.name = 'Pete'// delete user.name// defineProperty(user, 'name', ...)Object.defineProperty(user, 'name', { writable: true }); // 无效
💡 提示:“不可配置”不是“不可写”的意思
需要注意的是:一个不可配置但可写的属性,依然是可以被修改的。
configurable: false阻止的是属性被删除或属性标记被修改,并不影响属性可不可以被修改。
Object.defineProperties
看名字大家可能就能猜到与 Object.defineProperty 的关系了。Object.defineProperty 一次只能定义一个属性,而 Object.defineProperties(obj, descriptors) 允许一次同时定义多个属性。
语法如下:
Object.defineProperties(obj, {prop1: descriptor1,prop2: descriptor2,// ...});
这里 prop1、prop2 是要定义的属性,descriptor1、descriptor2 则是对应属性的描述符对象。
举个例子:
Object.defineProperties(obj, {name: { value: 'John', writable: false },surname: { value: 'Smith', writable: false },// ...});
如此,我们即能一次设置多个属性了。
Object.getOwnPropertyDescriptors
与上面同理,此方法与 Object.getOwnPropertyDescriptor 的不同之处在于,[Object.getOwnPropertyDescriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors) 方法用来一次性同时获得所有属性的描述符。
与 Object.defineProperties 一起使用,可以用来“克隆”对象。区别于普通的克隆方式,这可是“标志级别”的克隆方式:
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
通常克隆对象的方式如下:
for (let key in user) {clone[key] = user[key]}
但它并没有复制属性标志。如果我们想要“更好”的克隆,最好使用 所以使用 Object.defineProperties 的方式。
另外一个使用 for...in 循环的缺点是会忽略 Symbol 属性,但 Object.getOwnPropertyDescriptors 方法则会返回包含 Symbol 属性在内的 所有 属性描述符。
对象级别的访问限制
属性描述符是作用在单个属性上的。
下面列出了 JavaScript 提供的、作用于整个对象级别的访问限制方法。
[Object.preventExtensions(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions):阻止向对象添加属性。Object.seal(obj):阻止向对象添加属性,阻止删除属性。此方法会将对象的所有属性设置成不可配置的(configurable: false)。Object.freeze(obj):阻止向对象添加属性,阻止删除属性,阻止修改属性值。此方法会将对象的所有属性设置成不可配置的(configurable: false)和不可写的(writable: false)。
同时,还提供了针对这些方法的测试方法:
Object.isExtensible(obj):如果被阻止添加属性就返回false(说明对象不可扩展);否则返回true(说明对象是可扩展的)。Object.isSealed(obj):如果被阻止向对象添加属性和删除属性,并且对象当前的所有属性都是configuable: false的,就返回true(说明对象是封闭了的);否则返回false。Object.isFrozen(obj):如果被阻止向对象添加属性、删除属性和修改属性值,并且对象当前的所有属性都是configurable: false, writable: false的,就返回true(说明对象被冻结了);否则返回false。
但这些方法实际很少使用,了解一下便可。
(完)
📄 文档信息
**
🕘 更新时间:2020/01/09
🔗 原文链接:http://javascript.info/property-descriptors
