一、属性描述符基础
1.1 什么是属性描述符?
属性描述符是一个普通对象,用于描述一个属性的相关信息。在JavaScript中,每个属性都有一个对应的属性描述符,它定义了属性的行为特征,比如是否可写、是否可枚举、是否可配置等。
通过Object.getOwnPropertyDescriptor()
方法,我们可以获取一个对象某个属性的属性描述符:
js
let obj = { name: 'John' };
let descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
/*
{
value: "John",
writable: true,
enumerable: true,
configurable: true
}
*/
如果要获取对象所有属性的描述符,可以使用Object.getOwnPropertyDescriptors()
方法:
js
let descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);
1.2 属性描述符的类型
JavaScript中有两种类型的属性描述符:
- 数据属性描述符(Data Property Descriptor):用于描述拥有值的属性
- 存取器属性描述符(Accessor Property Descriptor):用于描述通过getter和setter函数访问的属性
两者的主要区别在于数据属性描述符直接存储值,而存取器属性描述符通过函数来控制值的获取和设置。
二、数据属性描述符详解
2.1 数据属性描述符的构成
一个数据属性描述符包含以下四个属性:
-
value:属性的值,可以是任何JavaScript数据类型
-
writable:布尔值,表示属性是否可写
true
:属性的值可以被修改(默认值)false
:属性的值是只读的,不能被修改
-
enumerable:布尔值,表示属性是否可枚举
true
:属性可以通过for...in
循环、Object.keys()
等方法枚举出来(默认值)false
:属性不会被枚举出来
-
configurable:布尔值,表示属性是否可配置
true
:属性的描述符可以被修改,属性可以被删除(默认值)false
:属性的描述符不能被修改,属性不能被删除
2.2 如何定义数据属性描述符
我们可以使用Object.defineProperty()
方法来定义或修改对象属性的描述符:
js
const obj = {};
Object.defineProperty(obj, 'name', {
value: '李四',
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.name); // 输出: 李四
obj.name = '王五'; // 尝试修改属性值,但无效
console.log(obj.name); // 输出: 李四 (属性值未被修改)
delete obj.name; // 尝试删除属性,但无效
console.log(obj.name); // 输出: 李四 (属性未被删除)
2.3 默认属性描述符
当我们直接给对象添加属性时,JavaScript会自动为该属性设置默认的描述符:
js
let obj = { a: 2 };
// 等同于
Object.defineProperty(obj, 'a', {
value: 2,
writable: true,
enumerable: true,
configurable: true
});
2.4 数据属性描述符的应用场景
-
保护属性 :通过设置
writable: false
,可以防止属性被意外修改,保证数据的安全性jsconst importantConfig = {}; Object.defineProperty(importantConfig, 'apiKey', { value: '123-456-789', writable: false });
-
隐藏属性 :通过设置
enumerable: false
,可以隐藏属性,使其不被for...in
循环等方法枚举出来,避免暴露内部实现细节jsfunction Cache() { Object.defineProperty(this, '_data', { value: {}, enumerable: false }); }
-
控制属性的删除和配置 :通过设置
configurable: false
,可以防止属性被删除或修改描述符,保证属性的稳定性jsclass Constants { constructor() { Object.defineProperty(this, 'PI', { value: 3.1415926, writable: false, configurable: false }); } }
三、存取器属性描述符详解
3.1 什么是存取器属性?
存取器属性是一种特殊的属性,它不直接存储值,而是通过getter和setter函数来控制属性的读取和设置。简单来说,存取器属性就像是对象属性的"门卫",你可以通过getter函数来定义读取属性时的行为,通过setter函数来定义设置属性时的行为。
3.2 存取器属性描述符的构成
存取器属性描述符主要关注以下两个属性:
-
get:用于读取属性的getter函数
- 当访问属性时调用
- 不能接收参数
- 应该返回一个值作为属性值
-
set:用于设置属性的setter函数
- 当给属性赋值时调用
- 接收一个参数(赋的值)
- 通常用于验证或转换值
注意:存取器属性描述符不能同时包含value
或writable
属性,否则会抛出错误。
3.3 如何定义存取器属性
定义存取器属性有两种主要方式:
3.3.1 使用Object.defineProperty()
方法
这是最常见也是最灵活的方式:
js
const person = {};
Object.defineProperty(person, 'fullName', {
get: function() {
return this.firstName + ' ' + this.lastName;
},
set: function(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
},
enumerable: true,
configurable: true
});
person.firstName = 'John';
person.lastName = 'Doe';
console.log(person.fullName); // 输出: John Doe
person.fullName = 'Jane Smith';
console.log(person.firstName); // 输出: Jane
console.log(person.lastName); // 输出: Smith
3.3.2 使用get和set关键字(ES6语法)
ES6引入了更简洁的语法来定义存取器属性:
js
const person = {
firstName: 'John',
lastName: 'Doe',
get fullName() {
return this.firstName + ' ' + this.lastName;
},
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(person.fullName); // 输出: John Doe
person.fullName = 'Jane Smith';
console.log(person.firstName); // 输出: Jane
console.log(person.lastName); // 输出: Smith
3.4 存取器属性的妙用
存取器属性的应用场景非常广泛,以下是一些常见的例子:
-
数据验证:可以在set函数中对传入的值进行验证,确保数据的有效性
jsconst circle = { _radius: 1, get radius() { return this._radius; }, set radius(value) { if (value <= 0) { throw new Error('Radius must be positive.'); } this._radius = value; } }; circle.radius = 5; // OK // circle.radius = -1; // 抛出错误
-
计算属性:可以根据其他属性的值动态计算出新的属性值
jsconst rectangle = { width: 10, height: 5, get area() { return this.width * this.height; } }; console.log(rectangle.area); // 输出: 50
-
实现数据绑定:访问器属性可以与框架或库结合使用,实现数据绑定功能,当属性值发生变化时,自动更新UI
-
日志记录:可以在getter和setter中添加日志记录功能,跟踪属性的访问和修改
jsclass Model { constructor() { this._data = null; } get data() { console.log('Getting data'); return this._data; } set data(value) { console.log('Setting data to', value); this._data = value; } }
-
懒加载:可以在getter中实现属性的懒加载,即第一次访问时才计算或获取值
jsclass ExpensiveObject { constructor() { this._value = null; } get value() { if (this._value === null) { console.log('Calculating expensive value...'); this._value = this._calculateValue(); } return this._value; } _calculateValue() { // 模拟耗时计算 let result = 0; for (let i = 0; i < 100000000; i++) { result += Math.sqrt(i); } return result; } }
3.5 存取器属性的注意事项
虽然存取器属性非常强大,但也需要注意一些潜在的陷阱:
-
无限循环 :如果在getter或setter中直接访问自身属性,可能会导致无限循环。因此,通常需要使用一个内部变量(例如
_radius
或_count
)来存储实际的值。js// 错误示例 - 会导致无限循环 const obj = { get value() { return this.value; // 会再次调用getter,导致无限循环 }, set value(v) { this.value = v; // 会再次调用setter,导致无限循环 } }; // 正确做法 const obj = { _value: null, get value() { return this._value; }, set value(v) { this._value = v; } };
-
性能问题:频繁调用getter和setter可能会影响性能,尤其是在复杂的计算场景下。需要根据实际情况进行权衡。
-
兼容性 :虽然现代浏览器都支持存取器属性,但在一些老旧浏览器中可能需要使用
Object.defineProperty
的polyfill。
四、属性描述符的高级应用
4.1 控制对象状态
JavaScript提供了3种方法,用来精确控制一个对象的读写状态,防止对象被改变:
-
Object.preventExtensions:阻止为对象添加新的属性
- 可以使用
Object.isExtensible()
检查对象是否可扩展
- 可以使用
-
Object.seal:阻止为对象添加新的属性,同时也无法删除旧属性
- 等价于
Object.preventExtensions(obj)
并设置现有的所有属性为configurable:false
- 可以使用
Object.isSealed()
检查对象是否被密封
- 等价于
-
Object.freeze:阻止为一个对象添加新属性、删除旧属性、修改属性值
- 效果相当于调用了
Object.seal(obj)
并设置所有属性为writable: false
- 可以使用
Object.isFrozen()
检查对象是否被冻结
- 效果相当于调用了
js
// 示例代码
var obj1 = {};
console.log(Object.isExtensible(obj1)); // true
Object.preventExtensions(obj1);
console.log(Object.isExtensible(obj1)); // false
var obj2 = {};
console.log(Object.isSealed(obj2)); // false
Object.seal(obj2);
console.log(Object.isSealed(obj2)); // true
var obj3 = {};
console.log(Object.isFrozen(obj3)); // false
Object.freeze(obj3);
console.log(Object.isFrozen(obj3)); // true
4.2 创建常量属性
通过组合writable: false
和configurable: false
,可以创建一个不能修改、重新定义或删除的对象常量属性:
js
var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});
myObject.FAVORITE_NUMBER = 100; // 修改无效
console.log(myObject.FAVORITE_NUMBER); // 42
delete myObject.FAVORITE_NUMBER; // 删除无效
console.log(myObject.FAVORITE_NUMBER); // 42
4.3 实现私有属性模式
虽然JavaScript没有真正的私有属性,但我们可以利用属性描述符模拟私有属性:
js
function createPrivateProperty(obj, name, initialValue) {
let value = initialValue;
Object.defineProperty(obj, name, {
get: function() {
console.log('Getting private property');
return value;
},
set: function(newValue) {
console.log('Setting private property');
value = newValue;
},
enumerable: false,
configurable: false
});
}
const obj = {};
createPrivateProperty(obj, '_secret', 'initial');
console.log(obj._secret); // "initial"
obj._secret = 'modified';
console.log(obj._secret); // "modified"
// 但这种方式并不是真正的私有,仍然可以通过obj._secret访问
4.4 属性描述符与原型链
当访问一个对象的属性时,JavaScript会沿着原型链查找。属性描述符在这过程中有一些特殊行为:
- 如果一个属性在原型链上被定义为只读(
writable:false
),你不能在子对象上创建同名属性 - 如果一个属性在原型链上是存取器属性,那么对该属性的读取或设置会调用原型上的getter/setter
js
const proto = {
get name() {
return 'Proto';
}
};
const obj = Object.create(proto);
obj.name = 'Obj';
console.log(obj.name); // 仍然是"Proto",因为原型上的name是只读的存取器属性
五、总结
- 数据属性描述符 直接存储值,并通过
value
、writable
、enumerable
和configurable
控制属性行为 - 存取器属性描述符通过getter和setter函数控制属性的访问和修改,不直接存储值
- 使用
Object.defineProperty
和Object.defineProperties
可以精细控制属性行为 Object.preventExtensions
、Object.seal
和Object.freeze
提供了不同级别的对象保护- 属性描述符是实现高级特性如数据验证、计算属性、数据绑定的基础