es-toolkit 的clone浅拷贝函数全解析:从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
clone是 es-toolkit 中用于创建对象浅拷贝(shallow copy)的核心函数。本文以 docs/ja/compat/reference/object/clone.md 为骨架,系统讲解es-toolkit/compat版clone的用法、支持的数据类型、浅拷贝语义,并结合源码剖析其内部实现原理、与标准版es-toolkit/object的clone差异,以及何时应改用cloneDeep与cloneWith,帮助你写出更高效、更安全的拷贝代码。
一、clone是什么:浅拷贝的基本语义
clone用于创建给定值的浅拷贝(shallow copy)。所谓浅拷贝,是指仅复制最顶层的属性,而嵌套的对象或数组会与原值共享引用。
const cloned = clone(value);用一句话概括其行为:
- 顶层值会生成新的实例(新数组、新对象、新 Date 等);
- 嵌套层级不会被递归复制,内部引用保持不变。
这一语义与 Lodash 的_.clone完全兼容,因此 es-toolkit 在compat包中提供了该函数的 Lodash 兼容版本,同时也提供了面向现代开发者的精简版本(见下文「五、compat版与现代版的对比」)。
二、es-toolkit/compat版clone的完整用法
2.1 基本调用方式
import { clone } from 'es-toolkit/compat'; // 原始值(primitive)的拷贝 const num = 42; const clonedNum = clone(num); // Returns: 42(同一个值) // 数组的拷贝 const arr = [1, 2, 3]; const clonedArr = clone(arr); // Returns: [1, 2, 3](新的数组实例) // 对象的拷贝 const obj = { a: 1, b: 'hello' }; const clonedObj = clone(obj); // Returns: { a: 1, b: 'hello' }(新的对象实例)2.2 支持复制特殊对象类型
compat版clone的一个重要特性是能够正确处理 JavaScript 中的各种内置对象类型:
import { clone } from 'es-toolkit/compat'; // Date 对象的拷贝 const date = new Date('2023-01-01'); const clonedDate = clone(date); // Returns: new Date('2023-01-01')(新的 Date 实例) // 正则表达式的拷贝 const regex = /hello/gi; regex.lastIndex = 3; const clonedRegex = clone(regex); // Returns: /hello/gi with lastIndex = 3(连 lastIndex 状态也会一并复制) // Map 的拷贝 const map = new Map([ ['a', 1], ['b', 2], ]); const clonedMap = clone(map); // Returns: new Map([['a', 1], ['b', 2]]) // Set 的拷贝 const set = new Set([1, 2, 3]); const clonedSet = clone(set); // Returns: new Set([1, 2, 3])注意正则表达式的细节:compat版在复制RegExp时,不仅复制source和flags,还会保留lastIndex状态。这意味着当你对RegExp使用了带g或y标志的exec/test后,其匹配位置信息也能被忠实地复制。
2.3 浅拷贝的关键特征:嵌套对象共享引用
import { clone } from 'es-toolkit/compat'; const nested = { a: 1, b: { c: 2, }, }; const clonedNested = clone(nested); console.log(clonedNested !== nested); // true(是不同的对象) console.log(clonedNested.b === nested.b); // true(嵌套对象是同一个引用)这是浅拷贝与深拷贝最本质的区别。如果你需要连嵌套对象也一起复制,应改用cloneDeep(见下文「六、与cloneDeep、cloneWith的协同使用」)。
2.4 参数与返回值
参数
value(T):要复制的值。可以是对象、数组、Date、RegExp、Map、Set、ArrayBuffer、DataView、类型化数组(TypedArray)、arguments对象、Symbol 包装对象以及各类原始值。
返回值
- (
T):复制后的值。对于原始值,直接返回原值本身。
三、compat版clone的源码级实现原理
compat版clone的实现位于 src/compat/object/clone.ts。它的实现思路是按值的类型分派处理,整体流程可以概括为以下几步:
3.1 第一步:原始值直接返回
export function clone<T>(obj: T): T { if (isPrimitive(obj)) { return obj; } // ... }代码首先调用 src/predicate/isPrimitive.ts 中的isPrimitive判断是否为原始值。string、number、boolean、null、undefined、symbol、bigint等原始值本身不可变,无需复制,直接返回即可。
3.2 第二步:通过getTag识别对象类型
对于非原始值,实现通过getTag(obj)(来自 src/compat/_internal/getTag.ts)获取对象的内部标签(如[object Date]、[object Map]等),标签常量定义在 src/compat/_internal/tags.ts。
随后调用isCloneableObject做白名单校验:只有arguments、array、arrayBuffer、dataView、boolean、date、各类 TypedArray、map、number、object、regexp、set、string、symbol等标签才被认为可克隆;函数、Promise、DOM 元素、Error 等类型不在白名单内,会直接返回{}(对应源码 src/compat/object/clone.ts 的isCloneableObject函数)。这也与测试 src/compat/object/clone.spec.ts 中「不克隆函数、async 函数、generator 函数、Proxy 构造函数、Error 系列」的用例一致。
3.3 第三步:按类型分派复制
isCloneableObject校验通过后,源码按类型逐一处理:
| 类型 | 复制方式 | 说明 |
|---|---|---|
数组Array | Array.from(obj) | 生成新数组;若为RegExp.exec的结果(含index/input属性),会额外复制这两个属性(src/compat/object/clone.ts) |
| 类型化数组 TypedArray | 用原构造器基于同一buffer重建 | 保持byteOffset与length,与buffer共享底层内存(src/compat/object/clone.ts) |
ArrayBuffer | new ArrayBuffer(byteLength) | 复制字节长度,但内容不复制(保持 Lodash 兼容行为) |
DataView | 复制字节到新 buffer 再重建 | 底层数据是真正复制的,修改副本不影响原值 |
Boolean/Number/String包装对象 | 用原构造器基于valueOf()重建 | 会同时复制自有属性(expando 属性),字符串包装对象还会复制length之外的字符串属性(src/compat/object/clone.ts) |
Date | new Date(Number(date)) | 复制时间戳 |
RegExp | 复制source+flags,并保留lastIndex | 见 src/compat/object/clone.ts |
Symbol包装对象 | Object(Symbol.prototype.valueOf.call(obj)) | 复制符号值包装对象 |
Map | 新建Map并遍历forEach逐项set | 键值引用不变(浅拷贝) |
Set | 新建Set并遍历forEach逐项add | 元素引用不变(浅拷贝) |
arguments对象 | 复制自有属性、length,并保留Symbol.iterator | 见 src/compat/object/clone.ts |
| 普通对象 | copyPrototype+copyOwnProperties+copySymbolProperties | 复制原型、自有属性与可枚举的 Symbol 属性 |
普通对象的复制由三个辅助函数协作完成(src/compat/object/clone.ts):
copyOwnProperties:遍历for...in并配合Object.hasOwn复制自有属性;copySymbolProperties:通过Object.getOwnPropertySymbols复制可枚举的 Symbol 属性;copyPrototype:通过Object.setPrototypeOf还原原型链,使clone(new Foo()) instanceof Foo仍为true。
从源码结构看,这套实现刻意对齐了 Lodash 的_.clone行为——包括arguments对象的Symbol.iterator保留、正则lastIndex保留、TypedArray 共享 buffer 等细节,都是为 Lodash 兼容场景而设计,因此也相对更重、更慢。
3.4 测试用例验证
src/compat/object/clone.spec.ts 提供了完整的兼容性测试覆盖,可以验证上述行为:
- 浅拷贝语义:
expect(actual).not.toBe(array)且expect(actual[0]).toBe(array[0])(L8-L15); arguments对象复制(含Symbol.iterator,L17-L42);Date、Map、Set、正则(含lastIndex)、数组的复制(L80-L169);ArrayBuffer按字节长度复制、DataView内容真实复制且互不影响(L197-L267);- 正则
exec结果数组的index/input属性保留(L269-L275); - 自定义类实例复制后仍保留原型(
instanceof Foo为true,L311-L331); - Symbol 属性只复制可枚举部分,不可枚举的 Symbol 不复制(L411-L434);
- 函数、Error、DOM 元素等不可克隆类型返回
{}(L499-L548)。
四、compat版的警告:优先使用现代版clone
compat 版文档 开头有一条醒目的警告:
这个
clone函数由于包含处理特殊对象类型的复杂逻辑而相对较慢。请使用更快速、更现代的 es-toolkit 的clone。
也就是说,如果你只是需要浅拷贝,且不依赖 Lodash 兼容语义,官方建议直接使用标准包的 clone(从es-toolkit/object导入)。compat包本身定位是「从 Lodash 迁移的兼容层」,其价值在于行为对齐,而非极致性能。
五、compat版与现代版的对比
es-toolkit 同时提供了两个版本的clone:
| 维度 | es-toolkit/compat的clone | es-toolkit/object的clone |
|---|---|---|
| 文档 | compat 版 | 现代版 |
| 源码 | src/compat/object/clone.ts | src/object/clone.ts |
| 定位 | 与 Lodash_.clone行为完全对齐 | 现代、精简、性能优先 |
| 原始值 | 直接返回原值 | 直接返回原值 |
| 数组 | 新数组实例,额外保留index/input | 新数组实例(slice(0)) |
| TypedArray / ArrayBuffer | 按类型精细处理 | 统一用slice(0)复制 |
Map/Set/Date | 分派处理 | new Constructor(obj)一行完成 |
RegExp | 复制source/flags/lastIndex | 复制source/flags/lastIndex |
Error | 不克隆,返回{} | 会复制 message、cause、stack 等 |
File(浏览器环境) | 不支持 | 支持(复制 name、type、lastModified) |
| 原型保留 | 通过copyPrototype实现 | 通过Object.create(prototype)+Object.assign实现 |
从源码对比可以看出(src/object/clone.ts),现代版用更少的代码覆盖了同样的核心类型:Date、Map、Set统一走new Constructor(obj),RegExp单独处理lastIndex,Error与浏览器File也都有专门分支。相比之下,compat版为了对齐 Lodash 在arguments、包装对象、Symbol 属性等边界场景的行为,逻辑分支明显更多、执行路径更长——这正是文档警告「相对较慢」的原因。
选型建议:
- 从 Lodash 迁移、需要逐行替换
_.clone的项目,用es-toolkit/compat; - 新项目或追求性能与包体积的场景,用
es-toolkit/object(或直接import { clone } from 'es-toolkit')。
六、与cloneDeep、cloneWith的协同使用
compat包在对象复制方向上还提供了深拷贝与自定义克隆两个相关函数,它们共享同样的 Lodash 兼容定位:
- cloneDeep:递归复制所有层级,嵌套对象与数组都会生成新实例。当你的数据是多层嵌套结构、且需要完全隔离时,应使用
cloneDeep而非clone; - cloneWith:允许传入自定义克隆函数,在默认复制行为之外注入自定义逻辑,适合需要定制拷贝策略(如按类型做特殊处理)的场景。
它们对应的现代版(es-toolkit/object的 cloneDeep、cloneDeepWith)同样遵循「现代版更精简、compat 版更对齐 Lodash」的整体设计原则。
七、实战要点速查
- 确认语义:
clone是浅拷贝,嵌套对象共享引用;需要完全隔离时改用cloneDeep。 - 原始值无需复制:
string/number/boolean/null/undefined/symbol/bigint直接返回原值。 - 内置类型安全复制:
Date、RegExp(含lastIndex)、Map、Set、ArrayBuffer、DataView、TypedArray、arguments对象均可正确处理。 - 不可克隆类型:
compat版对函数、Error、Promise、DOM 元素等返回{},切勿用于这些类型。 - 优先现代版:无 Lodash 兼容需求时,使用
es-toolkit/object的clone,更简洁也更快。 - 类型签名:
clone<T>(value: T): T,入参与返回值类型一致,天然保持类型安全。
结合本文的用法示例、源码解析与测试证据,你可以准确判断在什么场景使用clone、什么场景该升级为cloneDeep,并理解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),仅供参考