@eggjs/extend2 源码解析:Egg 框架配置合并与深拷贝的核心工具
发布时间:2026/9/20 19:57:05 锦皓数字建站

后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载导读eggjs/extend2是 Egg 框架仓库packages/extend2中一个轻量级但至关重要的工具包它是经典node-extendjQuery.extend 的 Node.js 移植版的衍生实现唯一且核心的差异在于深拷贝时把数组当作基本类型直接覆盖而不是按下标逐项合并。本篇文章将围绕这个差异展开先讲透extend的两种调用形态与深拷贝语义再结合 src/index.ts 的源码逐段拆解实现原理并用 test/index.test.ts 的测试用例验证各边界行为最后揭示它在 Egg 框架配置加载与 dump 中的真实应用。读完你将掌握如何正确使用extend(true, target, ...sources)做深合并、数组覆盖语义的实战价值、以及它和Object.assign、structuredClone的本质区别。一、包定位从一个关键差异说起eggjs/extend2的官方 READMEpackages/extend2/README.md开篇就一句话点明了它的来历与定位Forked from node-extend, the difference is overriding array as primitive when deep clone.即代码源自node-extend但与上游的唯一区别是——深拷贝deep clone模式下数组被当作基本类型处理直接整体覆盖而不会逐元素合并。为什么要做这个改动在 Egg 框架中extend2大量用于配置文件合并详见下文第六节。配置合并的场景里后加载的配置应该整体替换掉前者的同名数组通常是开发者更直觉的预期比如plugin.js里配的ignore列表、中间件数组如果逐下标合并很容易出现旧数组残留尾巴的隐蔽 Bug。把数组当作基本类型覆盖语义简单清晰也更容易推理。从包配置packages/extend2/package.json可以看到它的基本信息项目内容包名eggjs/extend2描述Port of jQuery.extend for Node.js关键词clone/extend/merge许可证MIT模块格式ESMtype: module导出开发期exports[.]指向./src/index.ts发布时指向./dist/index.js./dist/index.d.ts运行环境Node.js 22.18.0源码非常精简整个包只有一个核心文件 src/index.ts包含一个isPlainObject辅助函数和一个extend函数同时提供命名导出export { extend }与默认导出export default extend。二、基本用法两种调用形态README 给出了深拷贝合并的标准用法import { extend } from eggjs/extend2; // for deep clone extend(true, {}, object1, objectN);extend支持两种调用形态源码 src/index.ts 中通过参数类型自动识别// 形态一显式开启深拷贝 extend(deep, target, obj1, obj2, ...) // 形态二浅拷贝省略 deep 布尔参数 extend(target, obj1, obj2, ...)关键行为返回值返回合并后的target对象本身会原地修改target并不是返回新对象不传 targetextend(undefined, { a: 1 })会等价于extend({}, { a: 1 })自动以空对象作为目标多源合并支持obj1, obj2, ..., objN任意多个来源对象后面的覆盖前面的null/undefined 来源自动跳过options null || options undefined时直接continuesrc/index.ts。三、深拷贝的核心差异数组按基本类型覆盖先看一个直观对比。假设有两个对象const defaults { arr: [1, 2, 3] }; const override { arr: [x] };使用普通深合并工具如jQuery.extend、node-extend的深拷贝arr会按下标合并结果往往是[x, 2, 3]使用eggjs/extend2结果是{ arr: [x] }——数组被当作基本类型整体覆盖。这一行为被测试显式锁定在 test/index.test.tsit(deep clone; arrays are override, () { const defaults { arr: [1, 2, 3] }; const override { arr: [x] }; const expectedTarget { arr: [x] }; const target extend(true, defaults, override); assert.deepEqual(target, expectedTarget, arrays are merged); });测试断言target深等于{ arr: [x] }明确把数组覆盖而非数组合并写进了契约。这是本工具与所有逐项合并数组类深合并库最本质的分水岭。四、源码逐段拆解extend 的实现原理下面结合 src/index.ts 完整源码逐段解读。4.1 目标对象解析L30-L45let target deepOrTarget as any; let i 0; const length objects.length; let deep false; // Handle a deep copy situation if (typeof target boolean) { // extend(deep, target, obj1, obj2, ...) deep target; target objects[0] || {}; // skip the boolean and the target i 1; } else if ((typeof target ! object typeof target ! function) || target null) { // extend(null, obj1, obj2, ...) target {}; }第一个参数是boolean时视为深拷贝开关target顺延到第二个参数target缺省时兜底为{}第一个参数既不是对象也不是函数或为null同样兜底为{}——这就是extend(null, { a: 1 })也能正常工作的原因测试见 index.test.ts。4.2 浅拷贝与深拷贝的分支L47-L73for (; i length; i) { const options objects[i] as any; // Only deal with non-null/undefined values if (options null || options undefined) continue; // Extend the base object for (const name in options) { if (name __proto__) continue; const src target[name]; const copy options[name]; // Prevent never-ending loop if (target copy) continue; // Recurse if were merging plain objects if (deep copy isPlainObject(copy)) { const clone src isPlainObject(src) ? src : {}; // Never move original objects, clone them target[name] extend(deep, clone, copy); // Dont bring in undefined values } else if (typeof copy ! undefined) { target[name] copy; } } }几个值得注意的实现细节__proto__过滤遍历时显式跳过__proto__键L54这是针对原型污染攻击Prototype Pollution的安全加固。测试 index.test.ts 专门验证了这一点extend(true, {}, JSON.parse({__proto__: {polluted: yes}}))不会污染Object.prototype序列化结果仅为{}。自引用防死循环if (target copy) continue;L60——当来源属性值就是目标对象本身时跳过避免无限递归。只对 plain object 递归深拷贝分支要求isPlainObject(copy)成立L63数组、Date、类实例等统统不会进入递归这正是数组按基本类型覆盖的实现根源。只合并 plain object目标侧同样要求src是 plain object 才原地合并否则用{}作为新容器L64保证Never move original objects, clone them。浅拷贝模式不复制undefined非深拷贝分支typeof copy ! undefined才赋值L69即显式赋undefined的属性不会覆盖目标已有值。4.3 isPlainObject如何判定纯对象function isPlainObject(obj: unknown) { if (!obj || toStr.call(obj) ! [object Object]) { return false; } const hasOwnConstructor hasOwn.call(obj, constructor); const hasIsPrototypeOf obj.constructor obj.constructor.prototype hasOwn.call(obj.constructor.prototype, isPrototypeOf); // Not own constructor property must be Object if (obj.constructor !hasOwnConstructor !hasIsPrototypeOf) { return false; } let key: string | undefined; for (key in obj) { /* */ } return typeof key undefined || hasOwn.call(obj, key); }判定逻辑分三层先通过Object.prototype.toString判断[object Object]排除数组、Date、RegExp等这些返回[object Array]、[object Date]等再检查constructor与isPrototypeOf的来源排除挂载了自定义构造器属性但并非原生构造器的伪造对象最后遍历取最后一个键判断其是否为自有属性——利用自有属性优先枚举的引擎行为做快速判定注释Own properties are enumerated firstly, so to speed up。有趣的是测试 index.test.ts 构造的obj里故意放了constructor: fake和isPrototypeOf: not a function两个陷阱字段用来验证isPlainObject不会把这种长着奇怪属性、但本质仍是对象字面量的对象误判掉——它依然被当作可合并的 plain object 处理。4.4 边界行为一览来自测试矩阵test/index.test.ts 用完整的类型 × 类型组合矩阵锁定了行为契约摘录关键几条场景行为测试位置extend(undefined, {a:1})/extend({a:1})缺参兜底返回对象L41-L47extend()无参返回{}L46字符串/数字等基本类型作 target兜底为{}再合并L49-L187extend(false, defaults, override)浅拷贝正常覆盖L582-L587数组与数组浅拷贝下按下标合并就地修改第一个数组L209-L218深拷贝 数组数组整体覆盖L572-L580__proto__键跳过不污染原型L604-L608Array.isArray被禁用仍能正常工作实现不依赖它L595-L602深拷贝后修改目标来源不受影响递归处强制 clone不移动原对象L541-L568特别说明works without Array.isArray这条测试它把Array.isArray临时置为false再执行深拷贝验证实现不依赖Array.isArray判断数组——因为数组判断完全交由isPlainObject的toString标签完成。这是从 jQuery 一路传承下来的健壮性设计。五、与 Object.assign、structuredClone 的对比在日常开发中extend常与原生 API 对比三者的适用场景有明显差异能力extend本工具Object.assignstructuredClone深拷贝仅当deeptrue且只深合并 plain object不支持仅浅拷贝支持按结构化克隆算法多对象合并支持 N 个来源依次覆盖支持 N 个来源依次覆盖不支持只能克隆单个值原地修改目标是返回 target 本身是否返回全新对象数组语义深拷贝时整体覆盖引用覆盖深拷贝副本非 plain objectDate/类实例整体引用赋值引用赋值深拷贝副本自定义合并规则可自行用deep开关控制无无因此需要配置默认值 多环境覆盖 数组整体替换这类面向配置的场景extend2是比Object.assign太浅和structuredClone不合并、只克隆都更贴合的方案这也是 Egg 框架选择它的根本原因。六、在 Egg 框架中的真实应用eggjs/extend2并非孤立工具它直接服务于 Egg 框架的配置体系。仓库内共有两处核心引用6.1 配置加载器逐层合并 config 文件packages/core/src/loader/egg_loader.ts 在加载配置时多次调用extend(true, target, config)见 L1066、L1074、L1099 等处典型的合并链模式是extend(true, target, config); // 合并某份配置文件 extend(true, target, envConfig); // 再用环境配置覆盖最终形如config extend(true, {}, config)L1147——先以空对象为底座逐份把config.default、config.prod等合并进去实现默认值 → 环境值的多层覆盖并把结果继续灌入this.configMetaL1149用于记录配置来源元信息。6.2 配置 dump深拷贝快照packages/egg/src/lib/egg.ts 的dumpConfigToObject方法用extend(true, {}, { config, plugins, appInfo })生成一份深拷贝快照供dumpConfig()写入run/${type}_config.json。这里的语义是把this.config完整克隆到新的空对象里避免 dump 过程中任何一方被意外修改正对应源码注释 Never move original objects, clone them。可以看到框架对extend2的使用始终是extend(true, ...)深拷贝模式并且第一参数固定为{}或已有的 config 容器——这正与 README 给出的用法extend(true, {}, object1, objectN)完全一致。七、结语eggjs/extend2以不到 80 行的核心实现承载了 Egg 框架整个配置体系的合并与克隆需求。它继承自 jQuery.extend 的成熟算法通过深拷贝时数组按基本类型覆盖这一处刻意改造让配置合并的语义更贴近工程直觉同时内置__proto__防护、自引用检测、plain object 精确判定等安全与健壮性设计并有一套覆盖全部类型组合的测试矩阵背书。对于任何需要多源合并 深度克隆 数组整体覆盖的 Node.js/TypeScript 项目它都是一个经过生产验证的轻量选择。若想进一步研究可继续阅读完整实现packages/extend2/src/index.ts行为契约测试packages/extend2/test/index.test.ts包发布配置packages/extend2/package.json框架内应用示例packages/core/src/loader/egg_loader.ts 与 packages/egg/src/lib/egg.ts赞分享后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载相关推荐eggjs/extend2 深度解析Egg 框架配置合并背后的深拷贝与数组覆盖语义eggjs/extend2 深度解析Egg 框架配置合并背后的深拷贝与数组覆盖语义 eggjs/extend2 是 Egg 官方维护的 jQuery.ex后端Web框架Egg 框架文件监听插件 eggjs/watcher 实战与源码解析Egg 框架文件监听插件 eggjs/watcher 实战与源码解析 导读 eggjs/watcher 是 Egg 框架内置的文件监听插件为 Worker后端Web框架Goque完全指南基于LevelDB的持久化Go数据结构解决方案Goque完全指南基于LevelDB的持久化Go数据结构解决方案 Goque是一个为Go语言开发的嵌入式磁盘存储数据结构库它提供了基于LevelDB的持久化后端Web框架创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。