writable 和 enumerable 是 JavaScript 属性描述符的核心配置项,分别控制属性值是否可修改及是否可被遍历。通过 Object.defineProperty() 可设置 writable: false 防止属性值被更改,enumerable: false 使属性不在 for...in、Object.keys() 或 JSON.stringify() 中出现;直接赋值创建的属性默认两者均为 true,而 defineProperty 创建时未指定则默认为 false,这一差异需特别注意。精准控制这两项可提升代码安全性、封装性和可维护性,常用于定义不可变配置、隐藏内部状态、敏感数据保护及构建清晰的公共 API。

JavaScript 对象的属性描述符中的 writable 和 enumerable,是两个核心的配置项,它们决定了属性的修改权限和可枚举性,从而影响我们与对象属性的交互方式。简单来说,writable 控制属性值是否可被重新赋值,而 enumerable 则控制属性是否能在某些遍历操作(比如 for...in 循环或 Object.keys())中被发现。理解并恰当使用它们,能让我们对对象的行为有更精细的掌控。
在 JavaScript 中,每个对象属性都有一个与之关联的属性描述符(Property Descriptor)。这些描述符定义了属性的各种特性,其中 writable 和 enumerable 是我们经常需要打交道的两个。它们通过 Object.defineProperty() 方法来设置,或者通过 Object.getOwnPropertyDescriptor() 来查看。
writable 特性:控制属性值是否可写
当一个属性的 writable 特性被设置为 false 时,意味着这个属性的值不能通过简单的赋值操作被改变。尝试去修改它,在非严格模式下会静默失败(即不报错但修改无效),在严格模式下则会抛出 TypeError 错误。这对于我们希望保护某些配置、常量或者内部状态不被意外修改的场景非常有用。
考虑这个例子:
const config = {};
Object.defineProperty(config, 'API_KEY', {
value: 'some_secret_key_123',
writable: false, // 不可写
enumerable: true, // 可枚举,方便查看
configurable: false // 不可配置,更严格
});
console.log(config.API_KEY); // 输出: some_secret_key_123
// 尝试修改不可写的属性
config.API_KEY = 'new_key'; // 在非严格模式下静默失败,严格模式下会报错
console.log(config.API_KEY); // 仍然输出: some_secret_key_123
// 如果是严格模式,会报错:
// "use strict";
// config.API_KEY = 'new_key'; // TypeError: Cannot assign to read only property 'API_KEY' of object '#<Object>'在我自己的开发经验里,writable: false 经常用于定义一些全局的、不可变的环境变量或者模块内部的固定配置。它提供了一种比 const 声明更底层的不可变性控制,因为 const 只是保证变量绑定不被改变,而 writable: false 是针对对象属性值本身的。
enumerable 特性:控制属性是否可枚举
enumerable 特性决定了属性是否会在某些遍历操作中出现。当 enumerable 设置为 false 时,这个属性将不会被 for...in 循环、Object.keys()、Object.values()、Object.entries() 以及 JSON.stringify() 等方法识别和包含。但请注意,属性依然可以通过直接访问(例如 obj.propertyName)来获取其值。
这在很多场景下都非常有用,比如我们想在对象中存储一些内部的、不希望暴露给外部迭代或序列化的辅助属性。
const user = {
name: 'Alice',
age: 30
};
Object.defineProperty(user, 'internalId', {
value: 'user_xyz_123',
writable: false,
enumerable: false, // 不可枚举
configurable: false
});
console.log(user.internalId); // 可以直接访问:user_xyz_123
// 遍历对象属性
for (let key in user) {
console.log(key); // 只输出: name, age
}
console.log(Object.keys(user)); // 输出: ['name', 'age']
console.log(JSON.stringify(user)); // 输出: {"name":"Alice","age":30}我记得有一次在调试一个前端组件时,发现 JSON.stringify 出来的对象总是多了一些不该有的内部状态,排查后才发现是这些内部属性的 enumerable 特性没有被正确设置为 false。这让我意识到,在设计复杂对象时,对 enumerable 的精细控制是多么重要,它直接影响了数据的序列化和外部可见性。
默认行为的差异
值得一提的是,直接通过赋值语句创建的属性(例如 obj.prop = 'value')默认都是 writable: true, enumerable: true, configurable: true。而通过 Object.defineProperty() 创建属性时,如果没有明确指定这些描述符,它们的默认值则都是 false。这是一个常见的陷阱,很多人会因此感到困惑,为什么 defineProperty 后属性变得不可写或不可枚举了。
const myObj = {};
myObj.a = 1; // 默认 writable: true, enumerable: true, configurable: true
Object.defineProperty(myObj, 'b', {
value: 2 // 没有指定 writable, enumerable, configurable
});
// 此时 'b' 默认是 writable: false, enumerable: false, configurable: false
console.log(Object.getOwnPropertyDescriptor(myObj, 'a'));
// { value: 1, writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptor(myObj, 'b'));
// { value: 2, writable: false, enumerable: false, configurable: false }这个默认行为的差异,在我看来,是 JavaScript 在设计时的一种权衡。直接赋值是为了快速便捷,而 defineProperty 则更多是为了精确控制,所以默认值更偏向于“安全”和“限制”。
精确控制 JavaScript 对象的属性行为,远不止是语法上的一个技巧,它在实际开发中扮演着至关重要的角色。我个人认为,这主要关乎代码的健壮性、安全性、可维护性以及在特定场景下的性能考量。
首先,防止意外修改是核心原因之一。想象一下,你正在开发一个复杂的库或者框架,其中有一些内部状态或者配置项,它们在初始化后不应该被外部代码随意篡改。如果这些属性是 writable: true,那么任何地方的代码都可以不经意间改变它们,这可能导致难以追踪的 bug,甚至破坏整个系统的稳定性。通过将这些关键属性设置为 writable: false,我们就像给它们加了一把锁,确保了其值的完整性。这在构建不可变数据结构或者实现单例模式时尤其有用。
其次,控制数据的可见性和暴露程度。不是所有的对象属性都应该在每次遍历或序列化时被展示出来。例如,一个用户对象可能包含 name、email 等公共信息,但也可能包含 passwordHash、sessionToken 等敏感信息,或者 _internalCounter、_cache 等内部辅助属性。如果这些内部或敏感属性都是 enumerable: true,那么当我们将对象打印到控制台、通过 JSON.stringify 发送到后端,或者简单地用 for...in 循环时,它们就会暴露出来。这不仅可能造成安全隐患,也使得对象结构显得臃肿,增加了调试的复杂性。通过设置 enumerable: false,我们可以有效地“隐藏”这些属性,让对象的公共接口更加清晰,同时保护了内部实现细节。
再者,这种控制有助于提升代码的可读性和意图表达。当其他开发者(或者未来的你自己)看到一个属性被定义为不可写或不可枚举时,他们会立即明白这个属性的特殊性。这是一种非常明确的编程意图表达,减少了误解和误用。它告诉我们:“这个属性是设计来这样使用的,不要尝试去改变它,也不要期望它会出现在遍历中。”
最后,虽然不是最主要的原因,但在某些极端性能敏感的场景下,减少不必要的枚举操作也可能有微小的性能益处。但这通常不是我们考虑 enumerable 的首要驱动力,更重要的是逻辑和结构上的清晰。
理解 writable 和 enumerable 的默认值以及如何修改它们,是掌握属性描述符的关键一步。这其中存在一个非常重要的区别,取决于你创建属性的方式。
默认值:取决于属性的创建方式
通过直接赋值创建的属性: 当你像这样创建一个属性时:
const myObject = {};
myObject.myProp = 'Hello';这个 myProp 属性的 writable、enumerable 和 configurable 都会被默认设置为 true。这意味着 myProp 的值可以被修改,它会在 for...in 循环和 Object.keys() 中出现,并且其属性描述符本身也可以被修改(例如,你可以再次使用 Object.defineProperty 来改变它的 writable 状态)。
通过 Object.defineProperty() 创建的属性:
如果你使用 Object.defineProperty() 来创建属性,但没有明确指定 writable、enumerable 或 configurable,那么它们的默认值会是 false。这是一个非常常见的“陷阱”,很多人会在这里感到困惑。
const myObject = {};
Object.defineProperty(myObject, 'anotherProp', {
value: 'World' // 没有指定 writable, enumerable, configurable
});在这种情况下,anotherProp 的 writable、enumerable 和 configurable 都会是 false。这意味着它将是不可写的、不可枚举的,并且其描述符也无法再被修改。
我个人在初学 JavaScript 时,就曾因为 Object.defineProperty 的这个默认行为而感到困惑。当时我以为只要设置了 value,其他特性都会像普通属性一样是 true,结果发现属性无法修改也无法遍历,花了一段时间才搞清楚这个默认值的差异。
如何改变它们?
改变 writable 和 enumerable 的唯一官方方式就是使用 Object.defineProperty() 方法。这个方法允许你精确地定义或修改一个对象上属性的特性。
const product = {
id: 'p101',
name: 'Laptop'
};
// 假设我们想让 id 属性不可写
Object.defineProperty(product, 'id', {
writable: false // 将 id 设置为不可写
});
product.id = 'p102'; // 尝试修改,在非严格模式下静默失败
console.log(product.id); // 输出: p101
// 假设我们想添加一个不可枚举的内部序列号
Object.defineProperty(product, 'serialNumber', {
value: 'SN-XYZ-456',
writable: false,
enumerable: false // 设置为不可枚举
});
console.log(product.serialNumber); // 可以直接访问: SN-XYZ-456
console.log(Object.keys(product)); // 输出: ['id', 'name'] - serialNumber 不在其中需要注意的是,Object.defineProperty() 只能修改 configurable: true 的属性的描述符。如果一个属性的 configurable 已经是 false,那么你就无法再通过 Object.defineProperty() 来改变它的 writable 或 enumerable 状态了(除了将 writable 从 true 改为 false,这是一个例外)。这是一个更深层次的控制,通常用于创建非常“锁定”的属性。
除了 Object.defineProperty(),还有 Object.defineProperties() 方法,它允许你一次性为对象定义多个属性及其描述符,这在初始化一个包含多个特殊属性的对象时非常方便。
writable 和 enumerable 不仅仅是控制属性行为的基础工具,在一些更复杂的开发场景中,它们能够发挥出意想不到的威力,帮助我们构建更健壮、更灵活、更安全的系统。
1. 构建不可变配置或常量模块
在大型应用中,我们经常需要定义一些全局的、在运行时不应被修改的配置项或常量。直接使用 const 声明固然可以防止变量重新赋值,但如果 const 引用的是一个对象,对象的属性仍然是可变的。这时,writable: false 就派上了用场。
// constants.js
const CONFIG = {};
Object.defineProperties(CONFIG, {
API_BASE_URL: {
value: 'https://api.example.com/v1',
writable: false,
enumerable: true, // 方便查看
configurable: false
},
TIMEOUT_MS: {
value: 5000,
writable: false,
enumerable: true,
configurable: false
}
});
// 甚至可以进一步冻结整个对象,防止添加新属性或删除现有属性
Object.freeze(CONFIG);
export default CONFIG;
// 在其他模块使用
import CONFIG from './constants.js';
console.log(CONFIG.API_BASE_URL); // https://api.example.com/v1
// CONFIG.API_BASE_URL = 'new_url'; // 严格模式下会报错,非严格模式下静默失败通过这种方式,我们创建了一个真正意义上的不可变配置对象,任何试图修改其属性的行为都会被阻止,大大增强了代码的可靠性。
2. 实现“私有”或内部辅助属性
尽管 JavaScript 没有真正的私有属性(直到 ES2022 的 # 私有字段),但 enumerable: false 提供了一种模拟“私有”行为的有效方式,特别是对于那些不希望在外部被轻易发现或序列化的内部状态。
比如,在一个类或模块中,你可能需要一些只供内部逻辑使用的缓存、计数器或状态标识。
class DataStore {
constructor() {
this.data = [];
Object.defineProperty(this, '_lastFetchedTimestamp', {
value: Date.now(),
writable: true, // 内部可修改
enumerable: false, // 外部不可见
configurable: false
});
Object.defineProperty(this, '_updateCount', {
value: 0,
writable: true,
enumerable: false,
configurable: false
});
}
addData(item) {
this.data.push(item);
this._updateCount++; // 内部修改
this._lastFetchedTimestamp = Date.now();
}
toJSON() {
// 当序列化时,_lastFetchedTimestamp 和 _updateCount 不会被包含
return {
data: this.data
};
}
}
const store = new DataStore();
store.addData('item1');
store.addData('item2');
console.log(store._updateCount); // 仍然可以直接访问:2
console.log(Object.keys(store)); // 输出: ['data']
console.log(JSON.stringify(store)); // 输出: {"data":["item1","item2"]}这种模式在库和框架的内部实现中非常常见,它允许开发者在对象上存储必要的内部数据,同时保持公共 API 的整洁和清晰。
3. 数据序列化控制
enumerable: false 在数据序列化,特别是与 JSON.stringify() 配合时,表现得尤为强大。我们经常需要将 JavaScript 对象转换为 JSON 字符串进行网络传输或本地存储。如果对象中包含循环引用、函数、Symbol 或其他不适合 JSON 格式的数据,或者如前面提到的敏感信息,enumerable: false 可以帮助我们精确控制哪些属性应该被序列化。
const userProfile = {
username: 'john_doe',
email: 'john@example.com',
passwordHash: 'some_hash_value', // 敏感信息
getDisplayName() { return this.username; } // 函数
};
Object.defineProperty(userProfile, 'passwordHash', {
enumerable: false // 不希望被序列化
});
Object.defineProperty(userProfile, 'getDisplayName', {
enumerable: false // 函数默认就不会被 JSON.stringify 序列化,但明确设置可以增强意图
});
console.log(JSON.stringify(userProfile));
// 输出: {"username":"john_doe","email":"john@example.com"}
// passwordHash 和 getDisplayName 都没有被包含这种精细控制避免了在序列化时手动筛选属性的繁琐,使得数据传输更加安全和高效。
4. 框架和库的 API 设计
在设计复杂的 JavaScript 库或框架时,开发者经常会利用这些特性来构建更健壮和用户友好的 API。例如,一个库可能在返回给用户的对象上添加一些内部方法或属性,但又不希望这些方法或属性污染用户的 for...in 循环,或者被意外修改。
// 假设这是一个库内部的辅助函数
function createObservable(initialValue) {
let value = initialValue;
const subscribers = [];
const observable = {
get value() { return value; },
set value(newValue) {
value = newValue;
subscribers.forEach(cb => cb(value));
}
};
// 添加一个内部的订阅方法,不希望它被枚举
Object.defineProperty(observable, '_subscribe', {
value: (callback) => subscribers.push(callback),
writable: false,
enumerable: false, // 不可枚举
configurable: false
});
return observable;
}
const myObs = createObservable(10);
myObs._subscribe(val => console.log('New value:', val)); // 内部使用
console.log(Object.keys(myObs)); // 输出: ['value']
myObs.value = 20; // 输出: New value: 20这里,_subscribe 方法是 createObservable 内部机制的一部分,通过 enumerable: false,它不会在 Object.keys() 中出现,使得 observable 对象的公共接口看起来更简洁。
这些高级应用场景展示了 writable 和 enumerable 不仅仅是简单的开关,它们是构建复杂、安全和可维护 JavaScript 应用程序的强大基石。
以上就是JS 对象属性描述符 - 配置 writable、enumerable 的特性控制的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号