ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

jstips 第 39 期:用 Object.defineProperty 打造只读、私有与访问器属性

jstips 第 39 期:用 Object.defineProperty 打造只读、私有与访问器属性 教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载本指南围绕开源仓库 jstips 中第 39 期 JavaScript 技巧文档Advanced Properties展开系统讲解如何借助 ECMAScript 5.1 的Object.defineProperty/Object.defineProperties为对象配置伪私有、只读、隐藏属性并实现类似 OOP 语言的 getter/setter 与安全的深层属性访问别名。读完本文你将掌握描述符四大属性value、writable、enumerable、configurable的精确语义与组合用法并能直接在真实项目中复刻文中的所有代码示例。为什么需要高级属性JavaScript 的对象属性默认是公开、可写、可枚举、可重新配置的。在 ES5.1 之前开发者几乎无法限制属性的行为——任何代码都可以随意覆盖、删除或遍历对象上的数据。而Object.defineProperty的出现让这一切变得可控你可以配置对象属性例如将某个属性设为伪私有或只读。该特性自 ECMAScript 5.1 起可用因此所有现代浏览器均原生支持无需任何 polyfill 或第三方库。从仓库目录结构可以印证该主题的受关注程度_posts/en/javascript/、_posts/zh_CN/javascript/、_posts/zh_TW/javascript/、_posts/es_ES/javascript/四个语言版本均收录了同一篇 2016-02-08-advanced-properties.md而 README.md 的 Tips 列表中也将其登记为第 39 期Advanced Properties足见这是一个被社区反复校验过的核心技巧。第一个只读属性最小可运行示例原文给出的入门示例非常简洁它演示了赋值被静默忽略这一核心现象var a {}; Object.defineProperty(a, readonly, { value: 15, writable: false }); a.readonly 20; console.log(a.readonly); // 15代码要点拆解Object.defineProperty(a, readonly, {...})在对象a上定义名为readonly的属性描述符{ value: 15, writable: false }声明初始值为15且不可写第 6 行a.readonly 20的赋值操作不会生效因此第 7 行输出仍是15。需要补充一个原文未点破但极易踩坑的细节在非严格模式下对只读属性的赋值会静默失败不报错值不变而在use strict严格模式下同样的赋值会直接抛出TypeError。这与仓库另一篇技巧 use-strict-and-get-lazy 的主旨一脉相承——开启严格模式能让错误尽早暴露而不是被悄悄吞掉。因此在真实项目中若要依赖只读约束来防止 bug建议配合严格模式使用。语法速览单个定义与批量定义Object.defineProperty的标准调用形式如下Object.defineProperty(dest, propName, options)三个参数分别是目标对象dest、属性名字符串propName、描述符对象options。当需要一次性定义多个属性时使用Object.definePropertiesObject.defineProperties(dest, { propA: optionsA, propB: optionsB, //... })批量形式特别适合在对象初始化阶段集中声明一组具有不同特性的属性让哪些属性是只读、哪些是隐藏、哪些不可重定义一目了然。描述符四大属性详解options即属性描述符包含以下四个核心属性原文逐一给出了语义这里整理为速查表并补充取值边界描述符作用默认值注意事项value属性当前值。若非 getter见下文value是必填项undefined{a: 12}等价于Object.defineProperty(obj, a, {value: 12})writable是否可写false即只读false若属性值是嵌套对象其内部属性仍可被修改只读不深冻结enumerable是否可枚举false即隐藏falsefor...of与JSON.stringify均不会包含该属性但它依然存在且可被外部访问configurable是否可重新定义或删除false即锁定false嵌套对象内部的属性仍可各自配置锁的只是这一层描述符两个最常见的误解澄清enumerable: false不等于私有。原文特别强调That doesnt mean that the property is private! It can still be accessible from the outside。隐藏的只是被遍历、被序列化这件事属性本身依旧可以通过obj.propName直接读到——它只是不会被打印出来。只读与不可配置都不具备递归性。无论writable: false还是configurable: false都只作用于当前这个属性本身如果该属性的值是另一个对象那么那个对象的属性仍然是可写、可配置的。若要实现真正的深层次冻结需要配合仓库第 74 期技巧 Creating immutable objects in native JavaScript 中讲解的Object.freeze()、Object.seal()、Object.preventExtensions()逐层递归处理。组合使用打造伪私有常量将enumerable、writable、configurable三者同时置为false即可得到一个从外部完全无法枚举、无法改写、无法删除的私有常量属性Object.defineProperty(obj, myPrivateProp, {value: val, enumerable: false, writable: false, configurable: false});这一行代码的语义拆解value: val—— 赋予初始值enumerable: false—— 不出现在for...in/Object.keys()/JSON.stringify()的结果中writable: false—— 任何赋值尝试都无效严格模式下抛错configurable: false—— 无法被delete删除也无法再次defineProperty重定义。这与仓库第 74 期文档中Object.seal()与Object.freeze()的效果形成了梯度对比seal负责不可增删、不可改描述符freeze在seal基础上再加不可写而这里用defineProperty则可以把粒度精确到单个属性无需整对象锁死。动态属性名第二个参数是字符串Object.defineProperty的第二个参数propName是一个字符串因此它天然支持根据外部配置动态生成属性名。原文的对比示例非常直观// 字面量写法ES5.1 中非法因为键名不能是表达式 var obj { getTypeFromExternal(): true // illegal in ES5.1 }; // defineProperty 写法合法属性名由函数返回值决定 Object.defineProperty(obj, getTypeFromExternal(), {value: true}); // ok // 为了对比ES6 引入了计算属性名语法字面量也能写了 var obj { [getTypeFromExternal()]: true };这个特性的实用场景是当属性名来自服务端配置、用户输入或某个映射表时可以先用函数把配置项转换成属性名字符串再交给defineProperty落盘避免手写一长串if/else分支。getter 与 setter像 OOP 语言一样封装高级属性远不止锁属性这一种能力——它还能定义getter 与 setter这是面向对象语言中属性访问器的经典模式。此时描述符不再使用value与writable而是换成get与set函数function Foobar () { var _foo; // 真正的私有属性闭包变量 Object.defineProperty(this, foo, { get: function () { return _foo; }, set: function (value) { _foo value; } }); } var foobar new Foobar(); foobar.foo; // 触发 getter foobar.foo 20; // 触发 setter_foo 20两个需要特别说明的技术细节原文笔误修正原文在构造器内部写的是Object.defineProperty(obj, foo, ...)但obj在此作用域并不存在正确写法应为Object.defineProperty(this, foo, ...)上面代码已修正。结合上下文可以确定意图就是在new Foobar()生成的实例上定义foo访问器。描述符互斥规则value/writable与get/set是互斥的——同一个描述符中若同时出现value与get或writable与setObject.defineProperty会直接抛出TypeError。不过enumerable与configurable对两种描述符形态都是通用的依然可以组合使用。闭包变量_foo才是真正的私有数据外部代码永远无法直接触碰_foo只能通过get/set间接读写这就实现了原文所说的true private property——它比enumerable: false那种伪私有要彻底得多。安全的深层属性访问getter 别名的威力使用 getter 还有一个明显收益访问属性不需要加括号。foobar.foo看起来就是一次普通属性读取但背后会静默执行get函数。这让 getter 非常适合做安全访问别名。考虑一个多层嵌套对象var obj {a: {b: {c: [{d: 10}, {d: 20}] } } };直接访问obj.a.b.c[0].d时链路中任何一环为undefined都会抛出TypeError。利用 getter 短路求值可以定义一个永远不会抛错的别名属性Object.defineProperty(obj, firstD, { get: function () { return obj obj.a obj.a.b obj.a.b.c obj.a.b.c[0] obj.a.b.c[0].d } }); console.log(obj.firstD); // 10当中间某一环缺失时链会在该处短路返回undefined而不是抛错——这一模式与仓库第 27 期 Short circuit evaluation in JS 的技巧相辅相成。它本质上是把防御式取值封装成了属性本身让业务代码保持简洁。注意事项getter 无 setter 的赋值陷阱原文末尾的Note是一个实战高频坑值得单独强调如果定义了 getter 而没有定义 setter却仍然尝试给该属性赋值你会得到一个错误。Object.defineProperty(obj, foo, { get: function () { return 42; } }); obj.foo 100; // TypeError: Cannot set property foo of #Object which has only a getter这个错误在非严格模式与严格模式下都会抛出因为规范要求仅含 getter 的属性被赋值时必须报错。原文特别点出一个高危场景使用$.extend、_.merge这类对象合并工具函数时工具会把源对象的所有可枚举属性逐一拷贝到目标对象一旦目标对象上存在只有 getter 的属性合并过程就会触发赋值并抛错。因此在混入/合并类库处理的对象上定义 getter 时务必同时补一个 setter哪怕是空操作或者先通过Object.getOwnPropertyDescriptor检查目标对象的描述符形态再决定是否合并。与仓库其他技巧的呼应本技巧并非孤立存在jstips 仓库中与之形成知识闭环的还有第 74 期 Creating immutable objects in native JavaScript讲解Object.preventExtensions()、Object.seal()、Object.freeze()三个整对象级不可变手段其中Object.freeze()在内部正是把所有属性的描述符统一改为writable: false, configurable: false——与本篇的逐属性 defineProperty构成整对象 vs 单属性两种粒度第 07 期 Use strict and get lazy严格模式下只读/仅有 getter 属性的赋值会抛错建议在依赖高级属性的代码中开启第 27 期 Short circuit evaluation in JSgetter 别名中短路链的底层原理第 10 期 Check if a property is in an Object配合Object.getOwnPropertyDescriptor可精确判定属性的可写/可枚举状态。如果想用中文重温本篇可对照 zh_CN 译本 与 zh_TW 译本若想为仓库新增类似的属性技巧可参照 POST_TEMPLATE.md 的 front-matter 结构layout、title、tip-number、tip-tldr、categories提交新 tip。小结Object.defineProperty是 JavaScript 元编程能力的基石之一四个描述符开关value/writable/enumerable/configurable足以模拟只读、隐藏、不可重定义等属性约束get/set提供了类 OOP 的访问器封装字符串形式的属性名则带来了动态定义能力。理解这些语义不仅能写出更健壮的库代码也能在阅读 Vue、React 等框架源码时它们大量使用属性描述符做响应式劫持更快地抓住本质。赞分享教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载相关推荐jstips 第 40 期解读掌握 JSON.stringify 的附加参数按需序列化对象属性jstips 第 40 期解读掌握 JSON.stringify 的附加参数按需序列化对象属性 JSON.stringify 是 JavaScript 中最教程打造你的专属阅读空间Readest私有云阅读管家完整搭建指南打造你的专属阅读空间Readest私有云阅读管家完整搭建指南 Readest是一款现代化、功能丰富的电子书阅读器专为热爱阅读的用户设计提供无缝的跨平台访问桌面应用跨平台前端7步打造个人专属AI知识库LangChain数据访问与私有化部署指南7步打造个人专属AI知识库LangChain数据访问与私有化部署指南 你是否还在为企业数据无法接入AI而烦恼是否担心敏感信息上传云端的安全风险本文将带你通大模型教程提示工程RAG微调上一篇华硕笔记本性能优化终极指南如何用G-Helper替代臃肿的Armoury Crate下一篇AionUi 多代理模式实战3 步让 Gemini CLI、Claude Code、Codex 同屏并行干活创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进