es-toolkit 的 assignIn(extend):兼容 Lodash 的继承属性合并函数完全指南
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
导读
assignIn是 es-toolkit 在compat(Lodash 兼容)模块中提供的一个对象合并函数,其核心能力是将一个或多个源对象的自有属性与原型链上的继承属性一并复制到目标对象。本文以官方文档 docs/ja/compat/reference/object/assignIn.md(内容与英文版 docs/compat/reference/object/assignIn.md 一致)为主体,结合 assignIn 源码、keysIn 源码 与 单元测试,系统讲解其用法、与Object.assign/assign的差异、底层实现原理与适用场景。读完本文,你将掌握在 Lodash 迁移场景中何时使用assignIn、它如何工作,以及为什么官方建议优先使用原生Object.assign。
适用前提:
assignIn是 es-toolkit 为兼容 Lodash API 提供的函数,需要从es-toolkit/compat入口导入,并不包含在默认的es-toolkit主入口中。
一、函数概览与官方警告
文档开篇即给出一个醒目的警告(warning)区块,这是理解assignIn定位的关键:
建议使用
Object.assign这个assignIn函数由于需要额外处理继承属性的复制以及值比较逻辑,运行速度较慢。请改用更快、更现代的Object.assign。
也就是说,es-toolkit 提供assignIn的目的主要是保持与 Lodash 的 API 兼容性,方便旧代码平滑迁移,而非作为日常首选方案。其函数签名如下:
const result = assignIn(target, ...sources);功能定义:将源对象的所有属性(包括继承属性)分配给目标对象。
| 项目 | 说明 |
|---|---|
| 所属模块 | es-toolkit/compat(Lodash 兼容层) |
| 别名 | extend |
| 行为 | 复制源对象的自有属性 + 原型链继承属性到目标对象 |
| 返回值 | 修改后的目标对象(原地修改,返回同一引用) |
| 官方建议 | 优先使用原生Object.assign |
二、基础用法详解
2.1 基本复制
import { assignIn } from 'es-toolkit/compat'; // 基本用法 const target = { a: 1, b: 2 }; const source = { b: 3, c: 4 }; const result = assignIn(target, source); // 结果: { a: 1, b: 3, c: 4 } console.log(target === result); // true(目标对象被原地修改)从 assignIn 的实现 可以看到,该函数会遍历所有源对象逐个合并,并在最后return object,因此返回值就是被修改后的目标对象本身。
2.2 合并多个源对象
const target2 = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; assignIn(target2, source1, source2); // 结果: { a: 1, b: 2, c: 3 }多个源对象按参数顺序依次合并,后面的源对象会覆盖前面源对象或目标对象中的同名属性。
2.3 复制继承属性(与 assign 的核心区别)
function Parent() {} Parent.prototype.inherited = 'inheritedValue'; const child = Object.create(Parent.prototype); child.own = 'ownValue'; const target3 = {}; assignIn(target3, child); // 结果: { own: 'ownValue', inherited: 'inheritedValue' }child对象自身的own属性会被复制,同时其原型链上Parent.prototype的inherited属性也会被复制。这正是assignIn与assign最本质的区别——后者只会遍历自有属性(使用for...of Object.keys()语义),而assignIn会沿原型链向上遍历。
2.4 数组作为源对象
const arr = [1, 2, 3]; arr.customProp = 'custom'; const target4 = {}; assignIn(target4, arr); // 结果: { '0': 1, '1': 2, '2': 3, customProp: 'custom' }数组的索引属性'0'、'1'、'2'以及附加的自定义属性customProp都会被复制。这一行为背后依赖 keysIn 对类数组(array-like)对象的特殊处理:它会把数组视为稠密数组(dense array)来生成索引键。
三、参数与返回值
文档对参数的正式定义如下:
target(any):属性被复制到的目标对象。...sources(any[]):提供要复制属性的源对象,自有属性和继承属性都会被复制。
返回值:any——返回修改后的目标对象。目标对象本身被修改并返回。
对应到源码中的 TypeScript 重载签名(assignIn.ts),es-toolkit 针对 0 到 4 个源对象提供了精确的交叉类型推断:
export function assignIn<T, U>(object: T, source: U): T & U; export function assignIn<T, U, V>(object: T, source1: U, source2: V): T & U & V; export function assignIn<T, U, V, W>(object: T, source1: U, source2: V, source3: W): T & U & V & W; export function assignIn<T, U, V, W, X>(object: T, source1: U, source2: V, source3: W, source4: X): T & U & V & W & X; export function assignIn<T>(object: T): T; // 无源对象时原样返回目标对象 export function assignIn<R>(object: any, ...otherArgs: any[]): R; // 兜底重载- 不传任何源对象时,目标对象原样返回(
assignIn(target)的结果就是target本身)。 - 传入多个源对象时,返回类型是
T & U & V & ...的交叉类型,TypeScript 能精确推断合并结果的结构。
四、别名extend
在 Lodash 中,extend是assignIn的经典别名,es-toolkit 同样保留了这个别名。从源码看,extend.ts 只是一个再导出:
export { assignIn as extend } from './assignIn.ts';因此以下两种写法完全等价:
import { assignIn } from 'es-toolkit/compat'; import { extend } from 'es-toolkit/compat'; assignIn({}, source); extend({}, source); // 与上一行行为完全一致4.1 相关变体
在compat模块中还有一组与assignIn配套的函数,迁移时值得一并了解:
assignInWith:带自定义合并回调的版本(见 assignInWith.ts)。extendWith:assignInWith的别名(见 extendWith.ts)。assign/defaults/merge:仅处理自有属性或不覆盖已存在值的其他合并语义。
五、底层实现原理
5.1 主循环与单源合并
assignIn 的主函数 将每个源对象委托给assignInImpl处理:
export function assignIn(object: any, ...sources: any[]): any { for (let i = 0; i < sources.length; i++) { assignInImpl(object, sources[i]); } return object; } function assignInImpl(object: any, source: any): any { const keys = keysIn(source); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (!(key in object) || !eq(object[key], source[key])) { object[key] = source[key]; } } }注意这里有两个关键实现细节:
- 键来源是
keysIn而非Object.keys。keysIn使用for...in遍历,天然包含原型链上的可枚举属性,这是"复制继承属性"这一行为的技术根基。 - "值相同不覆盖"优化。条件
!(key in object) || !eq(object[key], source[key])表示:只有当目标对象中不存在该键、或目标值与源值不相等(eq比较,即Object.is语义)时,才执行赋值。这可以避免在属性值相等时触发不必要的 setter 或重复写入,是文档中提到的"值相同时不覆盖的优化"。
5.2 keysIn:继承属性遍历的基石
keysIn 的实现决定了assignIn对各种输入类型的处理行为:
null/undefined源对象:返回空数组,不复制任何属性。- 普通对象 / 函数:走
for...in收集字符串键(含原型链可枚举属性)。 - 原型对象(prototype)本身:会过滤掉
constructor键(prototypeKeysIn)。 - 类数组(array-like)对象:按稠密数组语义生成索引键,即稀疏数组
[1, , 3]会生成'0'、'1'、'2'三个键(缺失索引取undefined);同时针对 Buffer 与 TypedArray 保留了offset/parent/buffer/byteLength/byteOffset等非索引可枚举属性。 - 其他原始类型:先通过
Object(object)装箱再取键。
这也解释了文档示例中数组、字符串等"非常规对象"为何也能被正确合并——例如 测试用例 验证了assignIn({}, 'a')的结果是{ 0: 'a' }(字符串被当作类数组处理)。
5.3 与 assign 的对比
对比 assign 的实现,它的内部逻辑与assignInImpl几乎一致,唯一区别是键来源:assign使用keysToolkit(仅自有可枚举键),而assignIn使用keysIn(含继承键)。因此:
| 特性 | Object.assign | assign | assignIn/extend |
|---|---|---|---|
| 自有可枚举属性 | ✔ | ✔ | ✔ |
| 原型链继承属性 | ✘ | ✘ | ✔ |
| 值相同跳过写入 | ✘(总是赋值) | ✔(eq比较) | ✔(eq比较) |
| 原地修改目标 | ✔ | ✔ | ✔ |
| 性能 | 原生最快 | 较快 | 较慢(额外遍历 + 值比较) |
六、测试用例中的边界行为
assignIn.spec.ts 用 9 组用例锁定了该函数的行为契约,可作为实际使用时的行为参考:
- 基础赋值:
assignIn({ a: 1 }, { b: 2 })→{ a: 1, b: 2 }。 - 多源合并:后面的源对象覆盖前面已存在的键。
- 覆盖目标属性:源对象同名键覆盖目标值。
- nullish 值也会赋值:
{ a: null, b: undefined, c: null }会被正常复制(不会被跳过)。 - 值相同跳过写入:当目标对象属性带有 getter/setter 且值与源值相等时不会触发 setter——测试用抛错的 setter 验证了这一点(
{ a: 1, b: undefined, c: NaN, constructor: Object }均未触发异常,因为NaN === NaN在eq/Object.is语义下成立)。 - 稀疏数组按稠密处理:
[1, , 3]复制为{ 0: 1, 1: undefined, 2: 3 }。 - 原型对象赋值:
Foo.prototype.a = 1可被直接复制(且会过滤constructor)。 - 字符串源对象装箱:
assignIn({}, 'a')→{ 0: 'a' }。 - undefined 值属性正确保留:
{ workId: undefined }合并后键仍然存在。
这些用例对迁移 Lodash 代码尤其有价值:如果你的旧代码依赖"assignIn会连原型属性一起拷"或"undefined 值键不被丢弃"等 Lodash 语义,es-toolkit 的实现与测试保证了这些行为可被无缝继承。
七、使用建议与总结
- 默认使用
Object.assign:官方文档明确警告assignIn因继承属性遍历和值比较逻辑而变慢。绝大多数场景下,原生Object.assign或对象展开运算符{ ...source }更合适。 - 仅在兼容场景使用
assignIn:当你的代码库从 Lodash 迁移、且确实依赖"复制原型链上的可枚举属性"这一语义(例如合并带原型方法的配置对象、类实例属性)时,才需要从es-toolkit/compat引入assignIn。 - 牢记原地修改:
assignIn会直接修改并返回第一个参数target,如需保持原对象不变,请先传入空对象:assignIn({}, ...sources)。 - 善用类型推断:由于源码提供了交叉类型的重载签名,合并结果在 TypeScript 中能获得完整的类型提示,无需手动断言。
总而言之,assignIn(别名extend)是 es-toolkit 兼容层中一个"功能完整、语义精确但刻意不建议优先使用"的 Lodash 兼容函数。理解它的行为、源码与测试边界,能帮助你在 Lodash 迁移项目中做出正确的函数选型,并在真正需要继承属性合并时写出正确、可维护的代码。
延伸阅读
- assignIn 官方文档(英文)
- assignIn 源码
- keysIn 源码(继承属性遍历实现)
- assignIn 单元测试
- extend 别名源码
- compat 模块统一导出入口
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考