我将为您完善属性描述符的文档,详细介绍JavaScript对象属性的特性和描述符的使用方法。
---
title: 属性描述符
icon: javascript
order: 3
---
# 属性描述符
JavaScript对象的属性不仅有值,还有一系列特性控制它们的行为。本文将介绍属性描述符的概念、Object.defineProperty()方法的使用,以及如何创建具有特定特性的属性。
## 属性描述符基础
在JavaScript中,对象的每个属性除了有值外,还有三个特性(也称为属性描述符):
- **writable**:决定属性是否可以被修改
- **enumerable**:决定属性是否可以被枚举(如在for...in循环中出现)
- **configurable**:决定属性是否可以被删除或修改其特性
### 查看属性描述符
可以使用`Object.getOwnPropertyDescriptor()`方法查看属性的描述符:
```javascript
const person = {
name: '张三'
};
const descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor);
// 输出:
// {
// value: '张三',
// writable: true,
// enumerable: true,
// configurable: true
// }
查看对象的所有属性描述符:
const person = {
name: '张三',
age: 30
};
const descriptors = Object.getOwnPropertyDescriptors(person);
console.log(descriptors);
// 输出:
// {
// name: {
// value: '张三',
// writable: true,
// enumerable: true,
// configurable: true
// },
// age: {
// value: 30,
// writable: true,
// enumerable: true,
// configurable: true
// }
// }
数据属性与访问器属性
JavaScript中的属性分为两种类型:
1. 数据属性
数据属性包含一个值。它有以下特性:
- value:属性的值
- writable:是否可修改
- enumerable:是否可枚举
- configurable:是否可配置
2. 访问器属性
访问器属性不包含值,而是定义了getter和setter函数。它有以下特性:
- get:获取属性值的函数
- set:设置属性值的函数
- enumerable:是否可枚举
- configurable:是否可配置
使用Object.defineProperty()
Object.defineProperty()
方法允许精确地添加或修改对象的属性。
基本语法
Object.defineProperty(obj, prop, descriptor)
- obj:要定义属性的对象
- prop:要定义或修改的属性名
- descriptor:属性描述符对象
定义数据属性
const person = {};
Object.defineProperty(person, 'name', {
value: '张三',
writable: true,
enumerable: true,
configurable: true
});
console.log(person.name); // 输出:张三
定义访问器属性
const person = {
firstName: '三',
lastName: '张'
};
Object.defineProperty(person, 'fullName', {
get: function() {
return this.lastName + this.firstName;
},
set: function(value) {
const parts = value.split(' ');
this.lastName = parts[0];
this.firstName = parts[1];
},
enumerable: true,
configurable: true
});
console.log(person.fullName); // 输出:张三
person.fullName = '李 四';
console.log(person.lastName); // 输出:李
console.log(person.firstName); // 输出:四
属性特性的作用
writable(可写性)
控制属性值是否可以被修改:
const person = {};
Object.defineProperty(person, 'name', {
value: '张三',
writable: false, // 设置为只读
enumerable: true,
configurable: true
});
// 尝试修改属性值
person.name = '李四';
console.log(person.name); // 输出:张三(修改无效)
// 在严格模式下,修改只读属性会抛出错误
'use strict';
// person.name = '李四'; // TypeError: Cannot assign to read only property 'name'
enumerable(可枚举性)
控制属性是否出现在对象的属性枚举中:
const person = {};
Object.defineProperty(person, 'name', {
value: '张三',
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(person, 'age', {
value: 30,
writable: true,
enumerable: false, // 设置为不可枚举
configurable: true
});
// 使用for...in循环
for (const key in person) {
console.log(key); // 只输出:name
}
// 使用Object.keys()
console.log(Object.keys(person)); // 输出:['name']
// 直接访问仍然有效
console.log(person.age); // 输出:30
configurable(可配置性)
控制属性是否可以被删除或修改其特性:
const person = {};
Object.defineProperty(person, 'name', {
value: '张三',
writable: true,
enumerable: true,
configurable: false // 设置为不可配置
});
// 尝试删除属性
delete person.name;
console.log(person.name); // 输出:张三(删除无效)
// 尝试修改属性特性
try {
Object.defineProperty(person, 'name', {
writable: false
});
// 可以将writable从true改为false
Object.defineProperty(person, 'name', {
enumerable: false
});
// TypeError: Cannot redefine property: name
} catch (e) {
console.error(e.message);
}
使用Object.defineProperties()
可以一次定义多个属性:
const person = {};
Object.defineProperties(person, {
name: {
value: '张三',
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 30,
writable: false, // 只读
enumerable: true,
configurable: true
},
occupation: {
value: '工程师',
writable: true,
enumerable: false, // 不可枚举
configurable: true
}
});
console.log(person.name); // 输出:张三
console.log(person.age); // 输出:30
console.log(Object.keys(person)); // 输出:['name']
属性描述符的默认值
当使用Object.defineProperty()
定义属性时,如果没有指定某些特性,它们会有以下默认值:
- value:
undefined
- get:
undefined
- set:
undefined
- writable:
false
- enumerable:
false
- configurable:
false
const person = {};
// 没有指定特性
Object.defineProperty(person, 'name', {
value: '张三'
});
const descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor);
// 输出:
// {
// value: '张三',
// writable: false,
// enumerable: false,
// configurable: false
// }
而使用对象字面量或直接赋值创建的属性,所有特性默认为true
:
const person = {
name: '张三'
};
// 等同于
const person2 = {};
person2.name = '张三';
const descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor);
// 输出:
// {
// value: '张三',
// writable: true,
// enumerable: true,
// configurable: true
// }
实际应用场景
1. 创建常量属性
const settings = {};
Object.defineProperty(settings, 'API_URL', {
value: 'https://api.example.com',
writable: false,
enumerable: false,
configurable: false
});
// 尝试修改
settings.API_URL = 'https://new-api.example.com';
console.log(settings.API_URL); // 仍然是 'https://api.example.com'
2. 数据验证
const user = {
_age: 0
};
Object.defineProperty(user, 'age', {
get() {
return this._age;
},
set(value) {
if (typeof value !== 'number') {
throw new TypeError('年龄必须是数字');
}
if (value < 0 || value > 120) {
throw new RangeError('年龄必须在0到120之间');
}
this._age = value;
},
enumerable: true,
configurable: true
});
user.age = 30; // 正常
console.log(user.age); // 输出:30
try {
user.age = -5; // 抛出错误
} catch (e) {
console.error(e.message); // 输出:年龄必须在0到120之间
}
try {
user.age = '三十'; // 抛出错误
} catch (e) {
console.error(e.message); // 输出:年龄必须是数字
}
3. 计算属性
const rectangle = {
_width: 0,
_height: 0
};
Object.defineProperties(rectangle, {
width: {
get() {
return this._width;
},
set(value) {
if (value > 0) {
this._width = value;
}
},
enumerable: true,
configurable: true
},
height: {
get() {
return this._height;
},
set(value) {
if (value > 0) {
this._height = value;
}
},
enumerable: true,
configurable: true
},
area: {
get() {
return this._width * this._height;
},
enumerable: true,
configurable: true
}
});
rectangle.width = 5;
rectangle.height = 10;
console.log(rectangle.area); // 输出:50
rectangle.width = 7;
console.log(rectangle.area); // 输出:70
4. 私有属性模拟
function Person(name, age) {
// 公共属性
this.name = name;
// 私有属性
let _age = age;
// 定义age的getter和setter
Object.defineProperty(this, 'age', {
get() {
return _age;
},
set(value) {
if (value > 0) {
_age = value;
}
},
enumerable: true,
configurable: false
});
}
const person = new Person('张三', 30);
console.log(person.age); // 输出:30
// 无法直接访问_age
console.log(person._age); // 输出:undefined
属性描述符与对象不变性
属性描述符可以与其他方法结合,创建不同级别的对象不变性:
1. 防止扩展(Preventing Extensions)
Object.preventExtensions()
防止向对象添加新属性:
const person = {
name: '张三'
};
Object.preventExtensions(person);
// 尝试添加新属性
person.age = 30;
console.log(person.age); // 输出:undefined
// 检查对象是否可扩展
console.log(Object.isExtensible(person)); // 输出:false
2. 密封对象(Sealing)
Object.seal()
防止添加或删除属性,并将所有现有属性标记为不可配置:
const person = {
name: '张三',
age: 30
};
Object.seal(person);
// 可以修改现有属性
person.name = '李四';
console.log(person.name); // 输出:李四
// 不能添加新属性
person.occupation = '工程师';
console.log(person.occupation); // 输出:undefined
// 不能删除属性
delete person.age;
console.log(person.age); // 输出:30
// 不能修改属性描述符
try {
Object.defineProperty(person, 'name', {
enumerable: false
});
} catch (e) {
console.error(e.message); // TypeError: Cannot redefine property: name
}
// 检查对象是否被密封
console.log(Object.isSealed(person)); // 输出:true
3. 冻结对象(Freezing)
Object.freeze()
创建一个完全不可变的对象:
const person = {
name: '张三',
age: 30,
address: {
city: '北京'
}
};
Object.freeze(person);
// 不能修改属性
person.name = '李四';
console.log(person.name); // 输出:张三
// 不能添加属性
person.occupation = '工程师';
console.log(person.occupation); // 输出:undefined
// 不能删除属性
delete person.age;
console.log(person.age); // 输出:30
// 不能修改属性描述符
try {
Object.defineProperty(person, 'name', {
writable: true
});
} catch (e) {
console.error(e.message); // TypeError: Cannot redefine property: name
}
// 注意:freeze是浅冻结,嵌套对象仍然可以修改
person.address.city = '上海';
console.log(person.address.city); // 输出:上海
// 检查对象是否被冻结
console.log(Object.isFrozen(person)); // 输出:true
深度冻结对象
要创建完全不可变的对象,需要实现深度冻结:
// 深度冻结函数
function deepFreeze(obj) {
// 首先冻结对象本身
Object.freeze(obj);
// 获取所有属性,包括不可枚举的属性
const propNames = Object.getOwnPropertyNames(obj);
// 递归冻结每个属性值(如果是对象)
for (const name of propNames) {
const value = obj[name];
// 如果属性值是对象且未被冻结,则递归冻结
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
}
return obj;
}
// 使用示例
const person = {
name: '张三',
age: 30,
address: {
city: '北京',
district: '海淀'
},
contacts: [
{ type: 'email', value: 'zhangsan@example.com' },
{ type: 'phone', value: '123456789' }
]
};
// 深度冻结对象
deepFreeze(person);
// 尝试修改嵌套对象
person.address.city = '上海';
console.log(person.address.city); // 输出:北京(修改无效)
// 尝试修改数组中的对象
person.contacts[0].value = 'new-email@example.com';
console.log(person.contacts[0].value); // 输出:zhangsan@example.com(修改无效)
属性描述符与继承
属性描述符在处理对象继承时也很重要:
1. 继承属性的描述符
const parent = {};
// 在父对象上定义属性
Object.defineProperty(parent, 'name', {
value: '父对象',
writable: true,
enumerable: true,
configurable: true
});
// 创建继承自parent的对象
const child = Object.create(parent);
// 查看继承的属性
console.log(child.name); // 输出:父对象
// 尝试获取继承属性的描述符
console.log(Object.getOwnPropertyDescriptor(child, 'name')); // 输出:undefined
// 正确方式:先检查属性是否存在于对象本身
const descriptor = Object.getOwnPropertyDescriptor(child, 'name') ||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(child), 'name');
console.log(descriptor);
2. 覆盖继承的属性
const parent = {};
Object.defineProperty(parent, 'name', {
value: '父对象',
writable: true,
enumerable: true,
configurable: true
});
const child = Object.create(parent);
// 在子对象上定义同名属性
Object.defineProperty(child, 'name', {
value: '子对象',
writable: false, // 不同的描述符
enumerable: true,
configurable: false
});
console.log(parent.name); // 输出:父对象
console.log(child.name); // 输出:子对象
// 尝试修改子对象的只读属性
child.name = '新名称';
console.log(child.name); // 输出:子对象(修改无效)
属性描述符与Symbol属性
Symbol类型的属性也可以使用属性描述符:
const mySymbol = Symbol('description');
const obj = {};
Object.defineProperty(obj, mySymbol, {
value: '这是一个Symbol属性',
writable: true,
enumerable: false,
configurable: true
});
console.log(obj[mySymbol]); // 输出:这是一个Symbol属性
// Symbol属性默认是不可枚举的
console.log(Object.getOwnPropertySymbols(obj)); // 输出:[Symbol(description)]
console.log(Object.keys(obj)); // 输出:[](不包含Symbol属性)
// 获取Symbol属性的描述符
const descriptor = Object.getOwnPropertyDescriptor(obj, mySymbol);
console.log(descriptor);
// 输出:
// {
// value: '这是一个Symbol属性',
// writable: true,
// enumerable: false,
// configurable: true
// }
属性描述符的性能考虑
使用属性描述符可能会对性能产生影响:
// 性能测试:普通属性 vs 访问器属性
const normalObj = {};
const accessorObj = {};
// 定义10000个普通属性
console.time('普通属性');
for (let i = 0; i < 10000; i++) {
normalObj['prop' + i] = i;
}
console.timeEnd('普通属性');
// 定义10000个访问器属性
console.time('访问器属性');
for (let i = 0; i < 10000; i++) {
Object.defineProperty(accessorObj, 'prop' + i, {
get() { return i; },
enumerable: true,
configurable: true
});
}
console.timeEnd('访问器属性');
// 访问测试
console.time('访问普通属性');
let sum1 = 0;
for (let i = 0; i < 10000; i++) {
sum1 += normalObj['prop' + i];
}
console.timeEnd('访问普通属性');
console.time('访问访问器属性');
let sum2 = 0;
for (let i = 0; i < 10000; i++) {
sum2 += accessorObj['prop' + i];
}
console.timeEnd('访问访问器属性');
最佳实践
1. 何时使用属性描述符
- 需要创建只读属性时
- 需要隐藏某些属性不被枚举时
- 需要防止属性被删除时
- 需要实现计算属性或数据验证时
- 需要模拟私有属性时
2. 避免过度使用
- 属性描述符会增加代码复杂性
- 访问器属性比数据属性性能稍差
- 大量使用
Object.defineProperty()
会使代码难以阅读和维护
3. 使用简写语法
对于简单的getter和setter,可以使用对象字面量的简写语法:
// 使用Object.defineProperty
const person1 = {
_name: '张三'
};
Object.defineProperty(person1, 'name', {
get() {
return this._name;
},
set(value) {
this._name = value;
},
enumerable: true,
configurable: true
});
// 使用对象字面量简写语法
const person2 = {
_name: '张三',
get name() {
return this._name;
},
set name(value) {
this._name = value;
}
};
总结
属性描述符是JavaScript对象系统中的强大功能,它允许开发者精确控制对象属性的行为。通过合理使用属性描述符,可以:
- 保护数据:创建只读属性,防止意外修改
- 隐藏实现细节:使用不可枚举属性
- 防止删除关键属性:使用不可配置属性
- 实现数据验证:使用访问器属性验证输入
- 创建计算属性:使用getter实现动态计算的属性
- 模拟私有属性:结合闭包和访问器属性
属性描述符与Object.preventExtensions()
、Object.seal()
和Object.freeze()
等方法结合使用,可以创建不同级别的对象不变性,增强代码的健壮性和安全性。
在实际开发中,应根据具体需求选择合适的属性描述符,避免过度使用导致代码复杂化和性能下降。