0

0

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

幻影之瞳

幻影之瞳

发布时间:2025-09-24 23:23:01

|

1103人浏览过

|

来源于php中文网

原创

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 '#'

在我自己的开发经验里,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。这意味着它将是不可写的、不可枚举的,并且其描述符也无法再被修改。

    Lumen5
    Lumen5

    一个在线视频创建平台,AI将博客文章转换成视频

    下载

我个人在初学 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 应用程序的强大基石。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

559

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

438

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

776

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

481

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

574

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1091

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

659

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

554

2023.09.20

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

1

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.9万人学习

Swoft2.x速学之http api篇课程
Swoft2.x速学之http api篇课程

共16课时 | 0.9万人学习

PHP基础入门课程
PHP基础入门课程

共33课时 | 2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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