JavaScript具备很高的灵活性,你可以随便地对一个对象的属性成员进行赋值操作,且基本上不受变量类型的限制。同时,目前正式的ECMAScript标准中还没有对象私有成员的声明支持。JavaScript的属性成员总是会暴露给对象引用环境并且会被随意更改,这带来了很强的灵活性,在某些情景下,这带来了破坏性。当你在构造一个第三方JavaScript库,你可能需要暴露一些基本设施或配置给调用者来确定库函数运行的基本情况,而暴露的对象可能遭遇调用者的破坏性更改,或者以你不期望的方式被调用。好在JavaScript提供了更复杂精细的属性机制,通过这个机制,开发人员可以控制属性的访问权限和访问方式,来确保安全性。这个机制就是为对象设置属性描述符。
属性描述符
JavaScript对象属性描述符是一个描述某个属性访问情况的对象,属性描述符分为两种类型,数据描述符和访问器描述符。数据描述符是描述对象的某个属性的值(value)和属性值是否可写的(writable)、是否可配置(configurable)、是否可枚举(enumerable)的组合。访问器描述符通过getter函数/setter函数来描述某个属性的读写情况,同时访问器描述符也包括是否可配置(configurable)、是否可枚举(enumerable)的组合。
是否可配置(configurable)、是否可枚举(enumerable)属于两类属性描述符共同支持的属性描述符,对于它们分别单独支持的属性描述符,它们不可以同时去描述同一个属性。即,不可以既使用value或writable,同时使用getter函数或setter函数来描述同一个属性。这种情况下程序会报错。
如果描述符没有
value、writable、getter函数和setter函数键中的任何一个,它将被视为数据描述符。
| 属性描述符成员 | 数据描述符? | 访问器描述符? |
|---|---|---|
| value | ✅ | ❌ |
| writable | ✅ | ❌ |
| getter函数 | ❌ | ✅ |
| setter函数 | ❌ | ✅ |
| configurable | ✅ | ✅ |
| enumerable | ✅ | ✅ |
一个对象属性的属性描述符是一个对象,对象中的键值对描述了属性的访问情况,下面我们逐一介绍这些描述符键。
所有属性描述符键都是可选的,并且具备默认值。
value
属于数据描述符。它是与属性相关联的值。可以是任何有效的JavaScript值(数字、对象、函数等)。默认值为undefined。
writable
属于数据描述符。如果与属性相关联的值可以使用赋值运算符更改,则为true。默认值为false。当writable为false的时候,对属性的赋值操作会失败,非严格模式下会静默失败,严格模式下会报错。
get
属于访问器描述符。用作属性的getter函数,如果没有getter则为undefined。当访问该属性时,将不带参地调用此函数,并将this设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为undefined。
set
属于访问器描述符。用作属性setter的函数,如果没有setter则为undefined。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将this设置为通过该属性分配的对象,当属性的set为undefined时,赋值操作不会产生作用。默认值为undefined。
configurable
既属于数据描述符,又属于访问器描述符。当设置为false时,
- 该属性的类型不能在数据属性和访问器属性之间更改,且
- 该属性不可被删除(delete操作),且
- 其描述符的其他属性也不能被更改(但是,如果它是一个可写的数据描述符,则
value可以被更改,writable可以更改为false。即,configurable不影响value和writable)。 默认值为false。
enumerable
当且仅当该属性在对应对象的属性枚举中出现时,值为true。当属性可枚举时,当前属性是否可以在for...in循环和Object.keys()中被遍历出来,或者是否可以被Object.assign()或展开运算符所考虑。默认值为false。
设置属性描述符
在JavaScript中使用Object.defineProperty()和Object.defineProperties()设置属性描述符对象来定义或修改一个对象的属性,并返回这个对象。
Object.defineProperty()和Object.defineProperties()使用[[DefineOwnProperty]]内部方法,而不是[[Set]],因此即使属性已经存在,它也不会调用setter函数。
俩个函数的语法如下所示:
Object.defineProperty(obj, prop, descriptor);
Object.defineProperties(obj, props);
设置属性描述符的示例代码如下。
// 1. 指定是否可写
const obj1 = {};
Object.defineProperty(obj1, "property", {
value: 1,
writable: false
});
obj1.property = 5; // 非严格模式下会静默失败,严格模式下会报错
console.log(obj1.property); // 输出 1
// 2. 灵活使用访问器描述符
const obj2 = {};
let flag = 'red';
Object.defineProperty(obj2, "property", {
get: function() {
console.log('get flag: ' + flag);
return flag;
},
set: function(val) {
console.log('set flag, new flag value: ' + val);
flag = val;
}
});
console.log(flag); // 输出 "red"
console.log(obj2.property); // 输出 "get flag: red",然后输出 "red"
obj2.property = "blue"; // 输出 "set flag, new flag value: blue"
console.log(flag); // 输出 "blue"
console.log(obj2.property); // 输出 "get flag: blue",然后输出 "blue"
// 3. 同时设置多个属性描述符
const obj3 = {};
Object.defineProperties(obj3, {
property1: {
value: 2,
writable: true,
},
property2: {
value: "Hello",
writable: false,
},
property3: {
get() {
console.log('get property3 value: ' + this.property3Value);
return this.property3Value;
},
set(val) {
console.log('set property3 value, new value: ' + val);
this.property3Value = val;
}
}
});
有关默认值
注意,使用属性描述符符定义属性和直接定义属性在默认表现上是不同的。当你直接定义一个属性时,它的“默认表现”是可更改可配置可枚举的。当你使用属性描述符设置一个对象属性时,该属性默认值(缺省值)是不可更改不可配置不可枚举的。代码如下:
const obj1 = {};
obj1.a = 1;
// 等价于:
Object.defineProperty(obj1, "count", {
value: 1,
writable: true,
configurable: true,
enumerable: true,
});
// 另一种情况
const obj2 = {};
Object.defineProperty(obj2, "count", { value: 1 });
// 等价于:
Object.defineProperty(obj2, "count", {
value: 1,
writable: false,
configurable: false,
enumerable: false,
});
有关继承
有关继承有两个注意点:设置的属性描述符继承自其它对象时,以及包含属性描述符的对象作为其它对象的原型时。
当设置属性描述符继承自其它对象时,继承值也是有效的,所以我们需要避免原型中的值对配置的属性描述符的影响,可以使用Object.create(null)创建属性描述符或者向上面代码一样直接使用对象字面量。示例代码如下:
// 一个反例
const descriptorPrototype = {
get() {
return this.something;
},
set(val) {
this.something = val;
}
}
const descriptor = Object.create(descriptorPrototype);
descriptor.value = "content";
descriptor.writable = true;
const obj = {};
Object.defineProperty(obj, "content", descriptor); // 程序报错,descriptor 上同时存在 set/get 和 value/writable
我们在聊继承的时候(JavaScript 原型机制),提到过:“访问对象的属性能够从对象原型中查找,设置对象属性并不能。当使用赋值语句为对象设置属性值时,只会修改对象本身的属性或者添加这个属性”。但是访问器属性却可以完成继承,当使用原型上的访问器属性设置属性值时,这个变动对所有扩展对象生效。示例代码如下:
const objPrototype = {};
let flag = 'red';
Object.defineProperty(objPrototype, "flag", {
get() {
return flag;
},
set(val) {
flag = val;
}
});
const obj1 = Object.create(objPrototype);
const obj2 = Object.create(objPrototype);
console.log(flag); // 输出 "red"
console.log(obj1.flag); // 输出 "red"
console.log(obj2.flag); // 输出 "red"
obj1.flag = "blue";
console.log(flag); // 输出 "blue"
console.log(obj1.flag); // 输出 "blue"
console.log(obj2.flag); // 输出 "blue"
获取属性描述符
我们可以使用Object上的一些静态方法去获取属性描述符信息。
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor()静态方法返回一个对象,该对象描述给定对象上特定属性(即直接存在于对象上而不在对象的原型链中的属性)的配置。返回的对象是可变的,但对其进行更改不会影响原始属性的配置。在JavaScript中,一个属性由一个字符串值的名称或一个Symbol和一个属性描述符组成。
const obj1 = {
property1: 1,
};
const descriptor1 = Object.getOwnPropertyDescriptor(obj1, "property1");
console.log(descriptor1.configurable); // 输出 true
console.log(descriptor1.value); // 输出 1
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()静态方法返回给定对象的所有自有属性描述符。
const obj1 = {
property1: 1,
};
const descriptors1 = Object.getOwnPropertyDescriptors(obj1);
console.log(descriptors1.property1.writable); // 输出 true
console.log(descriptors1.property1.value); // 输出 1
其它帮助方法
基于属性描述符机制,JavaScript对对象属性的访问做更精细的控制。同时,JavaScript提供了一些帮助方法,用来简化这些复杂的基于属性描述符的操作。
Object.freeze(obj)
Object.freeze(obj)静态方法可以使一个对象被冻结。冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。Object.freeze(obj)返回与传入的对象相同的对象,它不会创建一个被冻结的副本。。冻结一个对象是JavaScript提供的最高完整性级别保护措施。
使用Object.isFrozen(obj)方法可以判断对象是否已冻结。
代码示例如下:
const obj = {
prop: 1,
};
Object.freeze(obj);
obj.prop = 2; // 非严格模式下静默失败,严格模式下报错
console.log(obj.prop); // 输出 1
浅冻结:调用
Object.freeze(obj)的结果仅适用于obj本身的直接属性,并且只会在obj上防止未来的属性添加、删除,或重新赋值的操作。如果这些属性的值本身是对象,这些对象不会被冻结,并且可能成为属性添加、删除,或重新赋值操作的目标。
尝试冻结带有元素的TypedArray或DataView会导致程序报错,因为它们是内存视图,可能会引起其他问题。
Object.seal(obj)
Object.seal(obj)静态方法密封一个对象。密封一个对象会阻止其扩展并且使得现有属性不可配置。密封对象有一组固定的属性:不能添加新属性、不能删除现有属性或更改其可枚举性和可配置性、不能重新分配其原型。只要现有属性的值是可写的,它们仍然可以更改。密封一个对象等价于阻止其扩展,然后将现有的属性描述符更改为configurable: false。Object.seal(obj)返回传入的同一对象。
使用Object.isSealed(obj)方法可以判断对象是否已密封。
const obj1 = {
property1: 1,
};
Object.seal(obj1);
obj1.property1 = 22;
console.log(obj1.property1); // 输出 22
console.log(Object.isSealed(obj1)); // 输出 true
delete obj1.property1; // 不能删除 property1
console.log(obj1.property1); // 输出 22
Object.preventExtensions(obj)
Object.preventExtensions()静态方法可以防止新属性被添加到对象中(即防止该对象被扩展)。它还可以防止对象的原型被重新指定。一般来说,不可扩展对象的属性仍然可以被删除。尝试向不可扩展对象添加新属性将静默失败,或在严格模式中抛出错误。同时,Object.preventExtensions()只能防止添加自有属性。但其对象类型的原型依然可以添加新的属性。
使用Object.isExtensible()静态方法可以判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
示例代码如下:
// 新对象是可拓展的。
const empty = {};
Object.isExtensible(empty); // true
// 它们可以变为不可拓展的
Object.preventExtensions(empty);
Object.isExtensible(empty); // false
// 根据定义,密封对象是不可拓展的。
const sealed = Object.seal({});
Object.isExtensible(sealed); // false
// 根据定义,冻结对象同样也是不可拓展的。
const frozen = Object.freeze({});
Object.isExtensible(frozen); // false