语言运行时嵌入式解释器【免费下载链接】duktapeDuktape - embeddable Javascript engine with a focus on portability and compact footprint项目地址https://gitcode.com/gh_mirrors/du/duktape点击查看免费下载导读Object.defineProperty()是 ECMAScript 中最复杂、最容易被误解的属性操作 API 之一它不仅要处理部分填充的属性描述符partial property descriptor还要兼顾 Array 的length越界写入、arguments 对象的[[ParameterMap]]魔法绑定、Proxy 陷阱等exotic behavior。本文以 Duktape 仓库中的设计文档 doc/hobject-alg-defineproperty.rst 为骨架逐层展开Object.defineProperty()的规范算法、内联化简过程并结合 src-input/duk_prop_defown.c 的源码实现与 src-input/duktape.h.in 的公开标志定义讲清 Duktape 如何在保持移植性的前提下把这套复杂算法落地为可维护、可验证的 C 代码。读完本文你将掌握属性描述符的校验规则、[[DefineOwnProperty]]的完整判定树以及duk_def_prop()C API 的底层语义。一、原始算法E5 Section 15.2.3.6 的五步骨架文档首先给出了规范层面的原始算法Original algorithm这是整个讨论的起点。Object.defineProperty()在 ES5 中的定义只有五步若Type(O)不是Object抛出TypeError。令name为ToString(P)的结果注意该转换可能产生副作用例如传入对象时触发其toString方法。以Attributes为参数调用ToPropertyDescriptor得到desc。以name、desc、true为参数调用O的内部方法[[DefineOwnProperty]]最后一个参数true是Throw标志。返回O。第 5 步返回对象本身使得该 API 天然支持链式调用。文档给出了示例var o {}; Object.defineProperty(o, foo, { value: bar } ).seal();这里Object.defineProperty()返回o因此可以紧接着调用o.seal()。这种返回被操作对象的设计是 ES5 属性 API 的一个实用特性。从实现角度看这五步中真正复杂的是第 3 步的ToPropertyDescriptor()与第 4 步的[[DefineOwnProperty]]。文档特别指出ToPropertyDescriptor()只被Object.defineProperty()和Object.defineProperties()调用它把以 ECMAScript 对象形式表达的描述符转换为规范描述符specification descriptor期间要完成布尔强制转换与交叉校验。最典型的校验是任何同时含有数据属性字段和访问器属性字段的描述符都会被拒绝。文档给出了 Node/V8 中的实际报错示例 var o {}; Object.defineProperty(o, foo, ... { value: bar, set: function() {} }); TypeError: Invalid property. A property cannot both have accessors and be writable or have a value, #Object两条补充说明对实现者至关重要键的强制转换是宽松的对象的强制转换是严格的P会通过ToString宽松转换可能触发用户代码副作用而O只做类型检查、绝不强制转换。[[DefineOwnProperty]]总是以Throw true调用因此实现层面无需暴露throw flag参数——失败即抛异常不存在静默失败分支。内部描述符的表示问题ToPropertyDescriptor()的输出是内部描述符internal descriptor。与读取已有属性得到的完整描述符不同这个内部描述符可能没有填满所有字段用户传入{ value: bar }时enumerable、configurable、writable字段都是缺失的。Duktape 的实现没有为这个仅存活于一次调用内的临时描述符显式建模而是用一组局部变量分别记录字段是否存在与字段强制转换后的值例如has_writable和is_writable是相互独立的两个变量。这一点在 src-input/duk_prop_defown.c 的头部注释中也有呼应[[DefineOwnProperty]]区别于大多数属性操作的地方就在于它处理的是可能部分填充的描述符——一个布尔位可能存在也可能整键缺失。二、算法内联First Draft 完整展开文档的第二个层次是First Draft把ToPropertyDescriptor()和带 exotic behavior 的[[DefineOwnProperty]]全部内联得到一个 33 步的完整算法。这个版本是理解 Duktape 源码的钥匙它揭示了数组越界写入、length写保护延迟生效等全部细节若Type(O)不是Object抛出TypeError。令name为ToString(P)可能有副作用。令desc为新的空 Property Descriptor。若O.[[HasProperty]](enumerable)为true令desc.[[Enumerable]]为ToBoolean(O.[[Get]](enumerable))。若O.[[HasProperty]](configurable)为true令desc.[[Configurable]]为ToBoolean(...)。若O.[[HasProperty]](value)为true令desc.[[Value]]为O.[[Get]](value)。若O.[[HasProperty]](writable)为true令desc.[[Writable]]为ToBoolean(...)。若O.[[HasProperty]](get)为true则令desc.[[Get]]为O.[[Get]](get)且若该值不是undefined且IsCallable(...)为false抛出TypeError。若O.[[HasProperty]](set)为true同理设置desc.[[Set]]并校验可调用性。若desc.[[Get]]或desc.[[Set]]任一存在且desc.[[Value]]或desc.[[Writable]]任一存在抛出TypeError数据/访问器混用校验。令Throw为true。令pendingWriteProtect为false。若O不是 Array 对象跳转 SKIPARRAY。调用[[GetOwnProperty]]取length的描述符oldLenDesc结果永不为undefined或访问器描述符因为 Array 的length数据属性不可删除、不可重配。令oldLen为oldLenDesc.[[Value]]保证是无符号 32 位整数。若P是length a. 若Desc.[[Value]]缺失跳转 SKIPARRAY b. 令newLen为ToUint32(Desc.[[Value]]) c. 若newLen不等于ToNumber(Desc.[[Value]])跳转 REJECTRANGE例如传入3.5这类非整数 d. 令Desc.[[Value]]为newLen e. 若newLen oldLen跳转 SKIPARRAY f. 若oldLenDesc.[[Writable]]为false跳转 REJECT g. 若Desc.[[Writable]]为false则延迟写保护令pendingWriteProtect为true并临时把Desc.[[Writable]]置为true——因为如果后面有元素删不掉length的缩短会部分失败此时写保护仍应生效 h. 跳转 SKIPARRAY剩余处理在 post-step。否则若P是数组索引E5 Section 15.4 a. 令index为ToUint32(P) b. 若index oldLen且oldLenDesc.[[Writable]]为false跳转 REJECT c. 跳转 SKIPARRAY。SKIPARRAY以P调用[[GetOwnProperty]]得到current。令extensible为O.[[Extensible]]。若current为undefined新属性 a. 若extensible为false跳转 REJECT b. 若IsGenericDescriptor(Desc)或IsDataDescriptor(Desc)为true创建数据属性缺失字段取默认值 c. 否则Desc必为访问器描述符创建访问器属性缺失字段取默认值 d. 跳转 SUCCESS。若Desc的每个字段都出现在current中且用SameValue算法E5 Section 9.12比较值都相同跳转 SUCCESS也覆盖Desc全空的场景。若current.[[Configurable]]为false a. 若Desc.[[Configurable]]为true跳转 REJECT b. 若Desc.[[Enumerable]]存在且与current的[[Enumerable]]互为布尔否定跳转 REJECT。若IsGenericDescriptor(Desc)为true跳转 VALIDATED。否则若IsDataDescriptor(current)与IsDataDescriptor(Desc)结果不同数据⇄访问器转换 a. 若current.[[Configurable]]为false跳转 REJECT b. 若current是数据属性转换为访问器属性保留[[Configurable]]、[[Enumerable]]其余取默认值 c. 否则转换为数据属性同样只保留 EC 两属性 d. 跳转 VALIDATED。否则若两者都是数据描述符 a. 若current.[[Configurable]]为false若current.[[Writable]]为false且Desc.[[Writable]]为true跳转 REJECT若current.[[Writable]]为false且Desc.[[Value]]存在而SameValue不等跳转 REJECT b. 跳转 VALIDATED。否则两者必都是访问器描述符 a. 若current.[[Configurable]]为false若Desc.[[Set]]存在且SameValue与当前不等跳转 REJECT对[[Get]]同理 b. 跳转 VALIDATED。VALIDATED对Desc中每个存在的字段把P属性对应属性值设为该字段值。SUCCESS若O是 Array a. 若P是length且newLen oldLen调用内部辅助函数ShortenArray()得到shortenSucceeded、finalLen把length更新为finalLen若pendingWriteProtect为true再把length的[[Writable]]置为false若shortenSucceeded为false跳转 REJECT b. 若P是数组索引且index oldLen把length更新为index 1必成功因为 pre-step 已确认length可写。若O是带[[ParameterMap]]的 arguments 对象 a. 若map上有P的 own property若IsAccessorDescriptor(Desc)为true以P、false调用map.[[Delete]]移除魔法绑定否则若Desc.[[Value]]存在以P、Desc.[[Value]]、Throw调用map.[[Put]]更新被绑定变量值若Desc.[[Writable]]存在且为false再以P、false调用map.[[Delete]]必须在变量值更新之后执行以移除魔法绑定。返回O。REJECT若Throw为true抛TypeError否则返回false。REJECTRANGE无条件抛RangeError即使Throw为false也抛。文档附注的两个简化观察文档给出了两个很有价值的实现观察第 3 步冗余它来自ToPropertyDescriptor()中对步骤 1 的重复因为Type(O)已经检查过。Throw恒为true步骤 12 可以直接删除步骤 32REJECT改为无条件抛TypeError。至于步骤 30 中把Throw作为[[Put]]参数传给参数映射实际上对行为没有影响——内部 setter 会被调用而Throw标志对 setter 不可见。三、清理后的算法Some cleanup在Some cleanup一节文档把上述 33 步整理为 31 步的干净版本实质改动只有三处删除冗余的Type(O)检查原第 3 步删除Let Throw be trueREJECT 分支直接无条件抛TypeError即原 33 步版本中步骤 12 与步骤 32 的合并arguments 参数映射中的[[Put]]调用统一使用true作为Throw参数。其余步骤编号整体前移pendingWriteProtect初始化变为步骤 11Array 前置处理从步骤 12 开始[[ParameterMap]]处理变为步骤 28REJECT/REJECTRANGE变为步骤 30/31。两份算法的判定逻辑完全等价清理版更适合直接对照源码阅读。由此可归纳出[[DefineOwnProperty]]的三条核心不变式与 src-input/duk_prop_defown.c 中duk__prop_defown_update_existing_slot的注释一一对应属性可配置configurable时任意修改都允许属性不可配置时任何修改都被拒绝仅有两类例外一是用SameValue()写入与现有属性相同的值二是把可写属性改为不可写单向反之不允许非配置属性上的数据⇄访问器转换、可枚举性翻转、访问器函数替换一律拒绝。Duktape 的实现把先校验后应用ValidateAndApplyPropertyDescriptor两步合并并用四路分支决定处理路径更新数据属性、更新访问器属性、数据转访问器、访问器转数据见duk__prop_defown_update_existing_slotsrc-input/duk_prop_defown.c。每种路径的校验都被内联这正是文档中把算法内联化简思想的代码化落地。四、源码对照从文档到 duk_prop_defown.c4.1 模块总览[[DefineOwnProperty]]的完整实现在 src-input/duk_prop_defown.c约 1520 行文件头部注释L1-L22明确写出三个设计约束Exotic behavior 特殊处理对象带 exotic[[DefineOwnProperty]]或属性以特殊内部形式存储时需要专门处理Force flag这是 Duktape 为 C API 添加的自定义特性允许在内部限制允许范围内如 Arraylength仅可控制 writability强制完成属性修改稳定性要求Proxy 链Proxy revocation 会使当前对象悬空与 Arguments 场景需要稳定化处理。4.2 描述符判定与字段标志规范中的IsGenericDescriptor/IsDataDescriptor/IsAccessorDescriptor在源码中由 duk__prop_is_accessor_descriptor 与 duk__prop_is_data_descriptor 实现它们直接检查DUK_DEFPROP_HAVE_*标志位。这些标志在公开头文件 src-input/duktape.h.in 中定义标志值含义DUK_DEFPROP_WRITABLE1U 0设置 writable需配合 HAVE 位DUK_DEFPROP_ENUMERABLE1U 1设置 enumerableDUK_DEFPROP_CONFIGURABLE1U 2设置 configurableDUK_DEFPROP_HAVE_SHIFT_COUNT8HAVE 位与值位的偏移量内部DUK_DEFPROP_HAVE_WRITABLE1U 8指定 writable 字段存在DUK_DEFPROP_HAVE_ENUMERABLE1U 9指定 enumerable 字段存在DUK_DEFPROP_HAVE_CONFIGURABLE1U 10指定 configurable 字段存在DUK_DEFPROP_HAVE_VALUE1U 11指定 value 字段存在值在值栈上DUK_DEFPROP_HAVE_GETTER1U 12指定 getter 存在DUK_DEFPROP_HAVE_SETTER1U 13指定 setter 存在DUK_DEFPROP_FORCE1U 14尽可能强制修改虚拟属性仍可能失败DUK_DEFPROP_THROW1U 15出错时抛异常内部值位 HAVE 位的双层设计恰好对应文档中has_writable与is_writable相互独立的局部变量方案DUK_DEFPROP_SET_WRITABLE是HAVE_WRITABLE | WRITABLE的组合而DUK_DEFPROP_CLEAR_WRITABLE只有HAVE_WRITABLE参见 duktape.h.in 中的SET_*/CLEAR_*便捷组合宏。新建槽位时缺失属性默认值为false实现直接利用HAVE掩码按位计算新属性字节new_attrs (defprop_flags (defprop_flags HAVE_SHIFT_COUNT)) DUK_DEFPROP_WEC见 duk__prop_defown_write_new_slot。4.3 数组length与延迟写保护文档中最具防坑价值的部分——数组length缩短时的pendingWriteProtect延迟写保护——在 duk__prop_defown_strkey_array_length 中完整保留。源码注释L634-L641概括了规范步骤的两个要点缩短length时如果存在index new_len的不可配置元素导致删除失败则停在那个位置并把length更新为匹配该元素的值写保护write protect即使只在部分成功甚至第一个元素就删不掉的情况下也要照常生效。代码中want_write_protect变量承担了文档里pendingWriteProtect的角色先判断(HAVE_WRITABLE | WRITABLE)组合是否为HAVE_WRITABLE即想清除 writable在length更新成功后调用DUK_HARRAY_SET_LENGTH_NONWRITABLE(a)。同时该函数还固化了length的固有约束不可配置化、不可枚举化、不可变为访问器以及非可写不可再写回可写的单向性DUK_HARRAY_LENGTH_NONWRITABLE检查。4.4 字符串对象与 Buffer 对象的虚拟属性文档虽以通用对象为主但源码还处理了两类虚拟属性String 对象的lengthduk__prop_defown_strkey_stringobj_length与 Buffer 对象的lengthduk__prop_defown_strkey_bufobj_length。二者都通过duk__prop_validate_immutable_data_descL131-L152检查描述符是否尝试修改不可变属性并允许SameValue相等的 value 写入通过——这正是文档中不可配置属性允许 SameValue 同值写入原则在虚拟属性上的延伸。4.5 Proxy 与 Arguments[[DefineOwnProperty]]的 exotic 分支还包括Proxyduk__prop_defown_strkey_proxyL719-L732检查defineProperty陷阱是否存在存在则通过duk__prop_defown_proxy_tail调用陷阱处理器并在DUK_USE_PROXY_POLICY配置下执行策略校验duk__prop_defown_proxy_policyArguments 对象对数组索引键走duk__prop_defown_idxkey_argumentsL1103对应文档中[[ParameterMap]]的魔法绑定移除/更新逻辑对非索引键则直接退化为普通OrdinaryDefineOwnProperty()见 L765-L769 的注释。整体调度由duk__prop_defown_strkey_helperL734与duk__prop_defown_idxkey_helperL1245按堆对象类型htype分派并通过duk__prop_defown_strkey_safe/duk__prop_defown_strkey_unsafeL26-L34提供带/不带 Proxy 稳定化的双通道入口。五、调用链从 JS 内建到 C API5.1Object.defineProperty与Reflect.defineProperty的统一入口在 src-input/duk_bi_object.c 中duk_bi_object_constructor_define_property通过函数 magic 值区分两个入口magic 0是Object.defineProperty()magic 1是Reflect.defineProperty()。其处理流程与文档的原始算法完全对应duk_require_hobject_promote_mask检查Type(O)并允许把轻量函数/普通 Buffer 提升为临时对象修改会丢失见注释 L493-L496duk_prop_topropdesc(thr)即ToPropertyDescriptor()的实现产出内部描述符并压入值栈以DUK_DEFPROP_THROW标记调用duk_prop_defown()对应Throw true。配套的duk_prop_topropdesc与duk_prop_frompropdesc描述符与defprop_flags双向转换位于 src-input/duk_prop_util.c是描述符对象与标志位表示之间的桥梁。5.2 C APIduk_def_propDuktape 把[[DefineOwnProperty]]的能力直接暴露给 C 嵌入方duk_def_propsrc-input/duk_api_object.c。调用时值栈布局为[ obj key ... ]根据标志位决定从栈上取 value / getter / setter。典型的便捷用法/* 定义普通数据属性 */ duk_push_int(ctx, 42); duk_def_prop(ctx, -2, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_WRITABLE | DUK_DEFPROP_SET_ENUMERABLE | DUK_DEFPROP_SET_CONFIGURABLE); /* 定义访问器属性attributes 恒为 0 */ duk_def_prop(ctx, -2, DUK_DEFPROP_HAVE_GETTER | DUK_DEFPROP_HAVE_SETTER | DUK_DEFPROP_FORCE);实现内部会检查is_data_desc flags (HAVE_VALUE | HAVE_WRITABLE)与is_acc_desc flags (HAVE_GETTER | HAVE_SETTER)来判定数据/访问器描述符L616-L617与规范中IsDataDescriptor/IsAccessorDescriptor的判定依据含 value 或 writable 即为数据描述符一致。DUK_DEFPROP_THROW会被自动附加确保 C 侧失败行为与 JS 语义对齐L653-L656。该 API 的实测用例可在 tests/api/test-def-prop.c、tests/api/test-def-prop-convenience.c 与 tests/api/test-def-prop-virtual.c 中查阅后者专门覆盖虚拟属性场景。5.3 内部算法的使用面[[DefineOwnProperty]]并不只为Object.defineProperty服务。设计文档 doc/hobject-alg-preliminaries.rst 列出了大量内部调用方包括属性读写体系FromPropertyDescriptor、[[Put]]、Array 的 exotic[[DefineOwnProperty]]E5 Section 15.4.5.1、arguments 对象的 exotic[[DefineOwnProperty]]E5 Section 10.6对象/环境初始化CreateMutableBinding、arguments 对象建立、数组/对象初始化器、函数对象创建、[[ThrowTypeError]]函数对象内建 APIObject.getOwnPropertyNames、Object.seal、Object.freeze、Object.keys、Function.prototype.bind数组与字符串方法Array.prototype.concat/slice/splice/map/filter、String.prototype.match/split、RegExp.prototype.exec序列化JSON.parse、JSON.stringify。正因使用面如此之广文档特别强调该算法需要为[[Put]]这类性能敏感路径做尽可能的内联见 doc/hobject-alg-preliminaries.rst 中 DefineOwnProperty callers 一节。Duktape 的[[Put]]路径因此拆出了两个极简特化版本更新已有属性已知属性存在、是数据属性、可写、描述符形如{[[Value]]: V}于是算法退化为数组 length 处理 直接写 value arguments 映射同步创建新属性已知属性不存在、对象可扩展、描述符四字段全为true于是退化为数组索引长度检查 新建槽位。这两条特化路径正是 src-input/duk_prop_set.c 中属性写入实现的理论基础。文档还给出了一个实现层面的务实提醒若新建属性因内存不足失败length可能已被提前更新因此建议先执行校验、再执行写入见 doc/hobject-alg-preliminaries.rst 中 DefineOwnProperty for a non-existent property in Put 一节的 Notes。六、验证与测试Duktape 为该功能提供了多层测试保障C API 层tests/api/test-def-prop.c 覆盖duk_def_prop的基础语义数据/访问器描述符、标志组合、错误路径tests/api/test-def-prop-convenience.c 覆盖SET_*/CLEAR_*便捷组合宏tests/api/test-def-prop-virtual.c 针对 String/Buffer 等虚拟属性的不可变约束ECMAScript 层tests/ecmascript 目录下的脚本测试例如与属性描述符相关的用例从 JS 侧验证Object.defineProperty/Object.defineProperties与Reflect.defineProperty的行为一致性运行方式可通过 examples/cmdline 中的命令行解释器按 dist-files/Makefile.cmdline 编译直接运行上述测试脚本或参照 util/runtest.py 批量执行。七、小结从 ES5 规范的五步骨架到 Duktape 的 33 步内联展开再到duk_prop_defown.c中四路分支 按位标志 延迟写保护的工程化实现Object.defineProperty()的完整图景可以概括为描述符是部分填充的has_*与is_*分离的表示对应DUK_DEFPROP_HAVE_*与DUK_DEFPROP_*双层位掩码是正确性的基石数组与 arguments 是两大 exotic 重灾区length缩短的pendingWriteProtect、索引越界自动扩展、[[ParameterMap]]魔法绑定移除都必须在通用算法之上额外处理不可配置属性不是不可修改它仍允许SameValue同值写入与可写→不可写的单向转变C API 与 JS API 共用同一内核Object.defineProperty与duk_def_prop最终都汇聚到duk_prop_defown()Throw恒真的约定让失败路径无需区分静默模式。对于需要为嵌入式环境编写属性管理代码、或希望深入理解 ECMAScript 属性语义的开发者本文所呈现的算法推导链规范 → 内联草稿 → 清理版 → 源码 → 测试是一份可直接对照仓库 doc/hobject-alg-defineproperty.rst 与 src-input/duk_prop_defown.c 反复研读的参考资料。赞分享语言运行时嵌入式解释器【免费下载链接】duktapeDuktape - embeddable Javascript engine with a focus on portability and compact footprint项目地址https://gitcode.com/gh_mirrors/du/duktape点击查看免费下载相关推荐IsaacLab 实战UR 机械臂 Robotiq 2F-85 夹爪完整配置5 步跑通稳定抓取仿真IsaacLab 实战UR 机械臂 Robotiq 2F 85 夹爪完整配置5 步跑通稳定抓取仿真 把 UR 机械臂和 Robotiq 夹爪的 URDF人工智能强化学习机器人具身智能深度学习React内部算法解析Under-the-hood-ReactJS展示Diff算法实现React内部算法解析Under the hood ReactJS展示Diff算法实现 React作为现代前端开发的核心框架其高效的 Diff算法 和 虚拟教程前端文档Lucky远程管理速成教程四步搞定端口转发控制台Lucky远程管理速成教程四步搞定端口转发控制台 软路由放在家里人在外面却改不了端口转发规则、盯不了 DDNS 同步状态家里电脑离线了也没法远程唤醒——这后端网络通信上一篇douyin-downloader 实战3 条命令保存抖音无水印视频批量下载作者作品下一篇OpenRig 上手指南安装、启动 kernel、组建第一支 agent 团队并交付第一个结果创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?