首页 > web前端 > js教程 > 正文

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

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

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

JavaScript 对象的属性描述符中的 writableenumerable,是两个核心的配置项,它们决定了属性的修改权限和可枚举性,从而影响我们与对象属性的交互方式。简单来说,writable 控制属性值是否可被重新赋值,而 enumerable 则控制属性是否能在某些遍历操作(比如 for...in 循环或 Object.keys())中被发现。理解并恰当使用它们,能让我们对对象的行为有更精细的掌控。

解决方案

在 JavaScript 中,每个对象属性都有一个与之关联的属性描述符(Property Descriptor)。这些描述符定义了属性的各种特性,其中 writableenumerable 是我们经常需要打交道的两个。它们通过 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,我们就像给它们加了一把锁,确保了其值的完整性。这在构建不可变数据结构或者实现单例模式时尤其有用。

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

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

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

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

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

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

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

    const myObject = {};
    myObject.myProp = 'Hello';
    登录后复制

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

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

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

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

    标书对比王
    标书对比王

    标书对比王是一款标书查重工具,支持多份投标文件两两相互比对,重复内容高亮标记,可快速定位重复内容原文所在位置,并可导出比对报告。

    标书对比王 58
    查看详情 标书对比王

我个人在初学 JavaScript 时,就曾因为 Object.defineProperty 的这个默认行为而感到困惑。当时我以为只要设置了 value,其他特性都会像普通属性一样是 true,结果发现属性无法修改也无法遍历,花了一段时间才搞清楚这个默认值的差异。

如何改变它们?

改变 writableenumerable 的唯一官方方式就是使用 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() 来改变它的 writableenumerable 状态了(除了将 writabletrue 改为 false,这是一个例外)。这是一个更深层次的控制,通常用于创建非常“锁定”的属性。

除了 Object.defineProperty(),还有 Object.defineProperties() 方法,它允许你一次性为对象定义多个属性及其描述符,这在初始化一个包含多个特殊属性的对象时非常方便。

在实际开发中,writable 和 enumerable 有哪些高级应用场景?

writableenumerable 不仅仅是控制属性行为的基础工具,在一些更复杂的开发场景中,它们能够发挥出意想不到的威力,帮助我们构建更健壮、更灵活、更安全的系统。

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 对象的公共接口看起来更简洁。

这些高级应用场景展示了 writableenumerable 不仅仅是简单的开关,它们是构建复杂、安全和可维护 JavaScript 应用程序的强大基石。

以上就是JS 对象属性描述符 - 配置 writable、enumerable 的特性控制的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号