对象属性操作:精细控制你的数据
在一栋公寓楼里,每个房间的门锁都有不同的权限设置。有些门可以自由进出,有些只能进不能出,还有些完全锁死。房东可能设置了某些房间不允许改造,某些房间的设施不能移除。JavaScript 对象的属性也有类似的权限系统——你可以控制属性是否可以被修改、删除、遍历,甚至可以在访问属性时执行自定义逻辑。这些精细的控制能力让我们能够构建更安全、更智能的数据结构。
属性描述符
每个对象属性都有一组隐藏的特性(attributes),这些特性决定了属性的行为。我们可以通过属性描述符(Property Descriptor)来查看和设置这些特性。
获取属性描述符
使用 Object.getOwnPropertyDescriptor() 可以获取单个属性的描述符:
let user = {
name: "Alice",
age: 25,
};
let descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// {
// value: "Alice",
// writable: true,
// enumerable: true,
// configurable: true
// }获取所有属性的描述符:
let allDescriptors = Object.getOwnPropertyDescriptors(user);
console.log(allDescriptors);
// {
// name: {
// value: "Alice",
// writable: true,
// enumerable: true,
// configurable: true
// },
// age: {
// value: 25,
// writable: true,
// enumerable: true,
// configurable: true
// }
// }属性特性详解
每个数据属性有四个特性:
value- 属性的值writable- 是否可以修改值enumerable- 是否可以在遍历中出现configurable- 是否可以删除属性或修改其特性
let product = {};
Object.defineProperty(product, "name", {
value: "Laptop",
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false, // 不可删除或重新配置
});
console.log(product.name); // "Laptop"
// 尝试修改 - 静默失败(严格模式下报错)
product.name = "Phone";
console.log(product.name); // "Laptop" - 未改变
// 尝试删除 - 失败
delete product.name;
console.log(product.name); // "Laptop" - 仍然存在defineProperty() - 定义单个属性
Object.defineProperty() 允许我们精确定义属性的特性。
基本用法
let account = {};
Object.defineProperty(account, "balance", {
value: 1000,
writable: true,
enumerable: true,
configurable: true,
});
console.log(account.balance); // 1000
account.balance = 1500;
console.log(account.balance); // 1500创建只读属性
let config = {};
Object.defineProperty(config, "API_KEY", {
value: "abc123xyz",
writable: false, // 只读
enumerable: true,
configurable: false,
});
console.log(config.API_KEY); // "abc123xyz"
// 无法修改
config.API_KEY = "newkey";
console.log(config.API_KEY); // "abc123xyz" - 未改变
// 无法删除
delete config.API_KEY;
console.log(config.API_KEY); // "abc123xyz" - 仍然存在创建不可枚举属性
不可枚举的属性不会出现在 for...in 循环、Object.keys() 或 Object.values() 中:
let person = {
name: "Bob",
age: 30,
};
// 添加一个不可枚举的属性
Object.defineProperty(person, "_id", {
value: 12345,
enumerable: false, // 不可枚举
writable: true,
configurable: true,
});
console.log(person._id); // 12345 - 可以访问
// 但在遍历中不可见
console.log(Object.keys(person)); // ["name", "age"]
for (let key in person) {
console.log(key); // 只打印 "name" 和 "age"
}
// 使用 Object.getOwnPropertyNames() 可以看到
console.log(Object.getOwnPropertyNames(person));
// ["name", "age", "_id"]这对于隐藏内部实现细节很有用。
实际应用:元数据存储
function addMetadata(obj, data) {
Object.defineProperty(obj, "__metadata", {
value: data,
writable: false,
enumerable: false, // 不在普通遍历中出现
configurable: false,
});
}
let user = {
name: "Alice",
email: "[email protected]",
};
addMetadata(user, {
createdAt: new Date(),
version: "1.0",
});
// 元数据可访问但不会干扰正常使用
console.log(user.__metadata);
// { createdAt: ..., version: "1.0" }
// 不会在序列化中出现
console.log(JSON.stringify(user));
// {"name":"Alice","email":"[email protected]"}
// 不会在遍历中出现
console.log(Object.keys(user)); // ["name", "email"]defineProperties() - 定义多个属性
Object.defineProperties() 可以一次定义多个属性:
let rectangle = {};
Object.defineProperties(rectangle, {
width: {
value: 100,
writable: true,
enumerable: true,
},
height: {
value: 50,
writable: true,
enumerable: true,
},
area: {
get() {
return this.width * this.height;
},
enumerable: true,
},
perimeter: {
get() {
return 2 * (this.width + this.height);
},
enumerable: true,
},
});
console.log(rectangle.width); // 100
console.log(rectangle.area); // 5000
console.log(rectangle.perimeter); // 300
rectangle.width = 150;
console.log(rectangle.area); // 7500 - 自动计算Getter 和 Setter
访问器属性(accessor properties)使用 getter 和 setter 函数来控制属性的读取和赋值。
基本 Getter 和 Setter
let user = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
},
};
console.log(user.fullName); // "John Doe"
user.fullName = "Jane Smith";
console.log(user.firstName); // "Jane"
console.log(user.lastName); // "Smith"
console.log(user.fullName); // "Jane Smith"使用 defineProperty 定义 Getter/Setter
let temperature = {
_celsius: 25, // 内部存储
};
Object.defineProperty(temperature, "celsius", {
get() {
return this._celsius;
},
set(value) {
if (value < -273.15) {
throw new Error("Temperature below absolute zero!");
}
this._celsius = value;
},
enumerable: true,
});
Object.defineProperty(temperature, "fahrenheit", {
get() {
return (this._celsius * 9) / 5 + 32;
},
set(value) {
this._celsius = ((value - 32) * 5) / 9;
},
enumerable: true,
});
console.log(temperature.celsius); // 25
console.log(temperature.fahrenheit); // 77
temperature.fahrenheit = 86;
console.log(temperature.celsius); // 30
console.log(temperature.fahrenheit); // 86
// 验证会生效
try {
temperature.celsius = -300;
} catch (e) {
console.log(e.message); // "Temperature below absolute zero!"
}实际应用:数据验证
function createUser(firstName, lastName, age) {
let user = {
_firstName: firstName,
_lastName: lastName,
_age: age,
};
Object.defineProperties(user, {
firstName: {
get() {
return this._firstName;
},
set(value) {
if (typeof value !== "string" || value.length === 0) {
throw new Error("First name must be a non-empty string");
}
this._firstName = value;
},
enumerable: true,
},
lastName: {
get() {
return this._lastName;
},
set(value) {
if (typeof value !== "string" || value.length === 0) {
throw new Error("Last name must be a non-empty string");
}
this._lastName = value;
},
enumerable: true,
},
age: {
get() {
return this._age;
},
set(value) {
if (typeof value !== "number" || value < 0 || value > 150) {
throw new Error("Age must be between 0 and 150");
}
this._age = value;
},
enumerable: true,
},
fullName: {
get() {
return `${this._firstName} ${this._lastName}`;
},
enumerable: true,
},
});
return user;
}
let user = createUser("Alice", "Johnson", 28);
console.log(user.fullName); // "Alice Johnson"
user.age = 29; // 有效
console.log(user.age); // 29
try {
user.age = 200; // 无效
} catch (e) {
console.log(e.message); // "Age must be between 0 and 150"
}懒加载属性
Getter 可以实现属性的懒加载——只在第一次访问时计算值:
let data = {
_cache: null,
get expensiveComputation() {
if (this._cache === null) {
console.log("Computing...");
// 模拟耗时计算
this._cache = Array.from({ length: 1000 }, (_, i) => i * i);
}
return this._cache;
},
};
console.log("First access:");
console.log(data.expensiveComputation.length); // Computing... 1000
console.log("Second access:");
console.log(data.expensiveComputation.length); // 1000 (不再计算)计算属性
let product = {
price: 100,
quantity: 5,
taxRate: 0.1,
get subtotal() {
return this.price * this.quantity;
},
get tax() {
return this.subtotal * this.taxRate;
},
get total() {
return this.subtotal + this.tax;
},
};
console.log(`Price: $${product.price}`);
console.log(`Quantity: ${product.quantity}`);
console.log(`Subtotal: $${product.subtotal}`); // $500
console.log(`Tax: $${product.tax}`); // $50
console.log(`Total: $${product.total}`); // $550
product.quantity = 10;
console.log(`New total: $${product.total}`); // $1100 - 自动更新属性存在性检查
检查属性是否存在有多种方法,每种都有其特定用途。
in 操作符
检查属性是否存在(包括继承的属性):
let obj = {
name: "Alice",
age: 25,
};
console.log("name" in obj); // true
console.log("email" in obj); // false
console.log("toString" in obj); // true - 继承自 Object.prototypehasOwnProperty / Object.hasOwn
只检查对象自身的属性:
let obj = {
name: "Alice",
};
console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("toString")); // false
// 推荐使用 Object.hasOwn (ES2022)
console.log(Object.hasOwn(obj, "name")); // true
console.log(Object.hasOwn(obj, "toString")); // false直接检查 undefined
let obj = {
name: "Alice",
email: undefined,
};
console.log(obj.name !== undefined); // true
console.log(obj.email !== undefined); // false
console.log(obj.phone !== undefined); // false
// 注意:这无法区分 undefined 值和不存在的属性
console.log("email" in obj); // true - 属性存在
console.log(obj.email !== undefined); // false - 但值是 undefined删除属性
使用 delete 操作符删除属性:
let user = {
name: "Bob",
age: 30,
email: "[email protected]",
};
delete user.email;
console.log(user); // { name: "Bob", age: 30 }
console.log(user.email); // undefined
// delete 返回布尔值
console.log(delete user.age); // true
console.log(delete user.phone); // true - 即使属性不存在
// 无法删除不可配置的属性
Object.defineProperty(user, "id", {
value: 123,
configurable: false,
});
console.log(delete user.id); // false - 无法删除
console.log(user.id); // 123 - 仍然存在属性枚举
控制属性是否在遍历中出现。
for...in 循环
遍历对象自身和继承的可枚举属性:
let parent = { inherited: "value" };
let child = Object.create(parent);
child.own = "own value";
for (let key in child) {
console.log(key); // 打印 "own" 和 "inherited"
}
// 只遍历自身属性
for (let key in child) {
if (Object.hasOwn(child, key)) {
console.log(key); // 只打印 "own"
}
}Object.keys/values/entries
只遍历对象自身的可枚举属性:
let obj = {
a: 1,
b: 2,
};
Object.defineProperty(obj, "c", {
value: 3,
enumerable: false,
});
console.log(Object.keys(obj)); // ["a", "b"]
console.log(Object.values(obj)); // [1, 2]
console.log(Object.entries(obj)); // [["a", 1], ["b", 2]]
// c 不在结果中Object.getOwnPropertyNames
获取所有自身属性(包括不可枚举的):
let obj = {
visible: 1,
};
Object.defineProperty(obj, "hidden", {
value: 2,
enumerable: false,
});
console.log(Object.keys(obj)); // ["visible"]
console.log(Object.getOwnPropertyNames(obj)); // ["visible", "hidden"]实战案例:响应式对象
创建一个简单的响应式系统,当属性改变时自动触发回调:
function createReactive(obj, onChange) {
let proxy = {};
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
let value = obj[key];
Object.defineProperty(proxy, key, {
get() {
return value;
},
set(newValue) {
let oldValue = value;
value = newValue;
onChange(key, oldValue, newValue);
},
enumerable: true,
configurable: true,
});
}
}
return proxy;
}
let state = createReactive(
{
count: 0,
message: "Hello",
},
(key, oldValue, newValue) => {
console.log(`${key} changed from ${oldValue} to ${newValue}`);
}
);
state.count = 1; // count changed from 0 to 1
state.message = "World"; // message changed from Hello to World
console.log(state.count); // 1实战案例:只读视图
创建对象的只读视图,防止意外修改:
function createReadOnlyView(obj) {
let proxy = {};
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
Object.defineProperty(proxy, key, {
get() {
return obj[key];
},
set() {
throw new Error(`Cannot modify readonly property "${key}"`);
},
enumerable: true,
});
}
}
return proxy;
}
let data = {
name: "Alice",
age: 25,
};
let readOnly = createReadOnlyView(data);
console.log(readOnly.name); // "Alice"
try {
readOnly.name = "Bob";
} catch (e) {
console.log(e.message); // Cannot modify readonly property "name"
}
// 但原对象仍可修改
data.name = "Charlie";
console.log(readOnly.name); // "Charlie" - 反映原对象的变化实战案例:属性访问日志
记录属性访问和修改:
function createLoggingProxy(obj, logFn) {
let proxy = {};
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
let value = obj[key];
Object.defineProperty(proxy, key, {
get() {
logFn("GET", key, value);
return value;
},
set(newValue) {
logFn("SET", key, value, newValue);
value = newValue;
},
enumerable: true,
});
}
}
return proxy;
}
let logs = [];
let user = createLoggingProxy(
{
name: "Alice",
age: 25,
},
(action, key, ...values) => {
logs.push({ action, key, values, timestamp: new Date() });
}
);
user.name; // GET
user.age = 26; // SET
user.name = "Bob"; // SET
console.log(logs);
// [
// { action: "GET", key: "name", values: ["Alice"], timestamp: ... },
// { action: "SET", key: "age", values: [25, 26], timestamp: ... },
// { action: "SET", key: "name", values: ["Alice", "Bob"], timestamp: ... }
// ]常见陷阱与最佳实践
1. configurable: false 是不可逆的
let obj = {};
Object.defineProperty(obj, "key", {
value: "value",
configurable: false,
});
// ❌ 无法重新配置
try {
Object.defineProperty(obj, "key", {
configurable: true,
});
} catch (e) {
console.log("Cannot redefine property"); // 会执行这里
}2. getter 没有 setter 时属性只读
let obj = {
get value() {
return 42;
},
};
console.log(obj.value); // 42
obj.value = 100; // 静默失败
console.log(obj.value); // 42 - 未改变3. 不要在 getter 中修改对象
// ❌ 不好的实践
let counter = {
_count: 0,
get count() {
return this._count++; // 每次访问都改变状态
},
};
console.log(counter.count); // 0
console.log(counter.count); // 1 - 令人困惑
// ✅ getter 应该是纯函数
let betterCounter = {
_count: 0,
get count() {
return this._count; // 只读取,不修改
},
increment() {
this._count++; // 通过方法修改
},
};4. 注意 this 绑定
let obj = {
value: 42,
get getValue() {
return this.value;
},
};
console.log(obj.getValue); // 42
let fn = obj.getValue;
console.log(fn); // undefined - this 丢失
// ✅ 使用箭头函数或 bind
let boundFn = obj.getValue.bind(obj);总结
对象属性操作为我们提供了精细的控制能力:
- 属性描述符 - 控制属性的
writable、enumerable、configurable特性 defineProperty/defineProperties- 精确定义属性及其特性- Getter/Setter - 在访问属性时执行自定义逻辑
- 属性枚举 - 控制哪些属性在遍历中可见
- 属性删除 - 使用
delete操作符或configurable特性
这些特性让我们能够:
- 创建只读或不可删除的属性
- 实现计算属性和数据验证
- 隐藏内部实现细节
- 构建响应式系统和数据代理
- 实现懒加载和缓存机制
掌握对象属性操作是编写健壮、可维护 JavaScript 代码的关键。虽然日常开发中可能不会频繁使用这些高级特性,但在构建框架、库或需要精确控制对象行为时,它们是不可或缺的工具。