JS 对象属性描述符 – 配置 writable、enumerable 的特性控制

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

JS 对象属性描述符 – 配置 writable、enumerable 的特性控制

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 对象的属性行为?

精确控制 JavaScript 对象的属性行为,远不止是语法上的一个技巧,它在实际开发中扮演着至关重要的角色。我个人认为,这主要关乎代码的健壮性、安全性、可维护性以及在特定场景下的性能考量。

首先,防止意外修改是核心原因之一。想象一下,你正在开发一个复杂的库或者框架,其中有一些内部状态或者配置项,它们在初始化后不应该被外部代码随意篡改。如果这些属性是 writable: true,那么任何地方的代码都可以不经意间改变它们,这可能导致难以追踪的 bug,甚至破坏整个系统的稳定性。通过将这些关键属性设置为 writable: false,我们就像给它们加了一把锁,确保了其值的完整性。这在构建不可变数据结构或者实现单例模式时尤其有用。

其次,控制数据的可见性和暴露程度。不是所有的对象属性都应该在每次遍历或序列化时被展示出来。例如,一个用户对象可能包含 name、email 等公共信息,但也可能包含 passwordHash、sessionToken 等敏感信息,或者 _internalCounter、_cache 等内部辅助属性。如果这些内部或敏感属性都是 enumerable: true,那么当我们将对象打印到控制台、通过 JSON.stringify 发送到后端,或者简单地用 for…in 循环时,它们就会暴露出来。这不仅可能造成安全隐患,也使得对象结构显得臃肿,增加了调试的复杂性。通过设置 enumerable: false,我们可以有效地“隐藏”这些属性,让对象的公共接口更加清晰,同时保护了内部实现细节。

再者,这种控制有助于提升代码的可读性和意图表达。当其他开发者(或者未来的你自己)看到一个属性被定义为不可写或不可枚举时,他们会立即明白这个属性的特殊性。这是一种非常明确的编程意图表达,减少了误解和误用。它告诉我们:“这个属性是设计来这样使用的,不要尝试去改变它,也不要期望它会出现在遍历中。”

最后,虽然不是最主要的原因,但在某些极端性能敏感的场景下,减少不必要的枚举操作也可能有微小的性能益处。但这通常不是我们考虑 enumerable 的首要驱动力,更重要的是逻辑和结构上的清晰。

writable 和 enumerable 默认值是什么?如何改变它们?

理解 writable 和 enumerable 的默认值以及如何修改它们,是掌握属性描述符的关键一步。这其中存在一个非常重要的区别,取决于你创建属性的方式。

默认值:取决于属性的创建方式

  1. 通过直接赋值创建的属性: 当你像这样创建一个属性时:

    const myObject = {}; myObject.myProp = 'Hello';

    这个 myProp 属性的 writable、enumerable 和 configurable 都会被默认设置为 true。这意味着 myProp 的值可以被修改,它会在 for…in 循环和 Object.keys() 中出现,并且其属性描述符本身也可以被修改(例如,你可以再次使用 Object.defineProperty 来改变它的 writable 状态)。

  2. 通过 Object.defineProperty() 创建的属性: 如果你使用 Object.defineProperty() 来创建属性,但没有明确指定 writable、enumerable 或 configurable,那么它们的默认值会是 false。这是一个非常常见的“陷阱”,很多人会在这里感到困惑。

    const myObject = {}; Object.defineProperty(myObject, 'anotherProp', {     value: 'World' // 没有指定 writable, enumerable, configurable });

    在这种情况下,anotherProp 的 writable、enumerable 和 configurable 都会是 false。这意味着它将是不可写的、不可枚举的,并且其描述符也无法再被修改。

    JS 对象属性描述符 – 配置 writable、enumerable 的特性控制

    Tripo AI

    AI驱动的3D建模平台

    JS 对象属性描述符 – 配置 writable、enumerable 的特性控制262

    查看详情 JS 对象属性描述符 – 配置 writable、enumerable 的特性控制

我个人在初学 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 有哪些高级应用场景?

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 应用程序的强大基石。

javascript word java js 前端 json 工具 session 后端 JavaScript json Object 常量 for 封装 const 字符串 循环 数据结构 接口 Property JS symbol 对象 严格模式 bug

上一篇
下一篇
text=ZqhQzanResources