Property descriptors
Property Descriptors
In JavaScript, object properties are not just simple key-value pairs. Each property is actually associated with a property descriptor object. Property descriptors define the behavioral characteristics of properties, including whether they are writable, enumerable, configurable, and more. Understanding property descriptors is key to mastering the JavaScript object model.
Types of Property Descriptors
Property descriptors are divided into two main types: data descriptors and accessor descriptors. Data descriptors directly define a property's value and its characteristics, while accessor descriptors control property access through getter and setter functions.
Data descriptors include the following optional keys:
value
: The value of the propertywritable
: Whether the property can be modifiedenumerable
: Whether the property is enumerableconfigurable
: Whether the property is configurable
Accessor descriptors include:
get
: A function to get the property valueset
: A function to set the property valueenumerable
: Whether the property is enumerableconfigurable
: Whether the property is configurable
// Data descriptor example
const obj1 = {};
Object.defineProperty(obj1, 'name', {
value: 'John',
writable: true,
enumerable: true,
configurable: true
});
// Accessor descriptor example
const obj2 = {};
let _age = 25;
Object.defineProperty(obj2, 'age', {
get() { return _age; },
set(newVal) { _age = newVal > 0 ? newVal : _age; },
enumerable: true,
configurable: false
});
Retrieving Property Descriptors
You can use the Object.getOwnPropertyDescriptor()
method to retrieve the descriptor of a specific property of an object:
const person = { name: 'Alice' };
const descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor);
/*
{
value: "Alice",
writable: true,
enumerable: true,
configurable: true
}
*/
To retrieve descriptors for all own properties of an object, use Object.getOwnPropertyDescriptors()
:
const car = {
brand: 'Toyota',
year: 2020
};
const descriptors = Object.getOwnPropertyDescriptors(car);
console.log(descriptors);
/*
{
brand: {
value: "Toyota",
writable: true,
enumerable: true,
configurable: true
},
year: {
value: 2020,
writable: true,
enumerable: true,
configurable: true
}
}
*/
Detailed Explanation of Property Attributes
Writable Attribute
When writable
is false
, the property value cannot be reassigned. In strict mode, attempting to modify it will throw an error:
const obj = {};
Object.defineProperty(obj, 'readOnly', {
value: 'initial',
writable: false
});
console.log(obj.readOnly); // "initial"
obj.readOnly = 'new value'; // Silently fails in non-strict mode
console.log(obj.readOnly); // Still "initial"
'use strict';
obj.readOnly = 'new value'; // TypeError: Cannot assign to read only property
Enumerable Attribute
enumerable
controls whether the property appears in for...in
loops and Object.keys()
:
const obj = {};
Object.defineProperties(obj, {
visible: { value: 1, enumerable: true },
hidden: { value: 2, enumerable: false }
});
console.log(Object.keys(obj)); // ["visible"]
for (let key in obj) {
console.log(key); // Only outputs "visible"
}
Configurable Attribute
When configurable
is false
, the property cannot be deleted, cannot be changed to an accessor property, and cannot modify attributes other than writable
and value
:
const obj = {};
Object.defineProperty(obj, 'x', {
value: 1,
configurable: false
});
// Attempt to delete
delete obj.x; // Silently fails
console.log(obj.x); // 1
// Attempt to modify attributes
Object.defineProperty(obj, 'x', {
enumerable: false
}); // TypeError: Cannot redefine property: x
// Only allowed modification
Object.defineProperty(obj, 'x', {
writable: false
}); // Succeeds
Use Cases
Creating Immutable Properties
function createImmutableObject(properties) {
const obj = {};
for (const [key, value] of Object.entries(properties)) {
Object.defineProperty(obj, key, {
value,
writable: false,
configurable: false
});
}
return obj;
}
const constants = createImmutableObject({
PI: 3.14159,
E: 2.71828
});
Implementing Private Property Patterns
function createPrivateProperties(obj, privateData) {
const privateStore = new WeakMap();
privateStore.set(obj, privateData);
for (const key in privateData) {
Object.defineProperty(obj, key, {
get() {
return privateStore.get(this)[key];
},
set(value) {
privateStore.get(this)[key] = value;
},
enumerable: true
});
}
return obj;
}
const user = {};
createPrivateProperties(user, {
_password: 'secret123',
_token: 'abc123'
});
console.log(user._password); // "secret123"
user._password = 'newSecret';
console.log(user._password); // "newSecret"
Prototype Chain and Property Descriptors
Property descriptors exhibit special behavior in the prototype chain. When accessing inherited properties through the prototype chain, their descriptor attributes affect how the properties are accessed:
const proto = {
inheritedProp: 'proto value'
};
Object.defineProperty(proto, 'readOnlyProp', {
value: 'cannot change',
writable: false
});
const child = Object.create(proto);
child.ownProp = 'child value';
console.log(child.inheritedProp); // "proto value"
child.inheritedProp = 'new value'; // Actually creates a new property on child
console.log(child.inheritedProp); // "new value"
console.log(proto.inheritedProp); // Still "proto value"
child.readOnlyProp = 'attempt change'; // Silently fails
console.log(child.readOnlyProp); // "cannot change"
Batch Property Definition
Object.defineProperties()
allows defining multiple properties at once:
const config = {};
Object.defineProperties(config, {
apiUrl: {
value: 'https://api.example.com',
writable: false,
enumerable: true
},
maxRetries: {
value: 3,
writable: true
},
logLevel: {
get() {
return this._logLevel || 'info';
},
set(value) {
if (['debug', 'info', 'warn', 'error'].includes(value)) {
this._logLevel = value;
}
},
enumerable: true
}
});
Combining with Class Syntax
In ES6 classes, you can use accessor properties and static methods with property descriptors:
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
}
static createReadOnly(name, value) {
const temp = new Temperature(0);
Object.defineProperty(temp, name, {
value,
writable: false,
configurable: false
});
return temp;
}
}
const temp = Temperature.createReadOnly('unit', 'Celsius');
console.log(temp.unit); // "Celsius"
temp.unit = 'Fahrenheit'; // Silently fails
Limitations of Property Descriptors
Certain operations are restricted by property descriptors:
- Cannot change a non-configurable property from a data property to an accessor property, or vice versa
- Cannot modify the
enumerable
attribute of a non-configurable property - For data properties, if
writable
isfalse
andconfigurable
isfalse
, thevalue
cannot be modified - If an accessor property's
configurable
isfalse
, theget
andset
functions cannot be modified
const obj = {};
Object.defineProperty(obj, 'x', {
value: 1,
writable: false,
configurable: false
});
// The following operations will all throw TypeError
Object.defineProperty(obj, 'x', {
get() { return 2; }
});
Object.defineProperty(obj, 'x', {
enumerable: false
});
Object.defineProperty(obj, 'x', {
value: 2
});
Combining with Proxy
Property descriptors can be combined with Proxy to implement advanced property control:
const handler = {
defineProperty(target, prop, descriptor) {
if (prop.startsWith('_')) {
throw new Error(`Cannot define private property "${prop}"`);
}
// Force all properties to be non-enumerable
return Reflect.defineProperty(target, prop, {
...descriptor,
enumerable: false
});
}
};
const target = {};
const proxy = new Proxy(target, handler);
proxy.publicProp = 'value'; // Succeeds
console.log(Object.keys(proxy)); // []
try {
proxy._privateProp = 'secret'; // Error: Cannot define private property "_privateProp"
} catch (e) {
console.error(e.message);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn