Package detail

@qubit-ltd/clone

Haixing-Hu136Apache-2.01.13.1

A JavaScript library provides a deep clone function

readme

@qubit-ltd/clone

npm package License English Document CircleCI Coverage Status

clone 是一个 JavaScript 库,用于深度克隆 JavaScript 对象。它保持对象及其所有属性的原型, 并支持自定义克隆钩子函数,允许对指定类型执行特殊的克隆算法。

不同于内置的 structuredClone() 函数,此开源库具有以下特性:

  • 深度克隆:能够深度克隆任意 JavaScript 对象,包括但不限于简单对象、自定义类的实例、 ArrayMapSetDateRegExpErrorPromise 等。
  • 保持原型:克隆的对象保持原有对象的原型。
  • 循环引用检测:能够检测循环引用,并防止无限递归。
  • 支持内置对象的自定义属性:由于 JavaScript 语言的灵活性,对于内置对象,用户可以在该对象 上任意设置自定义属性,例如const str = 'hello'; str.x = 123;,本函数库能够克隆这些自定 义属性。
  • 可自定义的克隆算法参数:支持自定义克隆算法参数,通过参数定制克隆算法。
  • 可自定义的命名转换规则:支持自定义命名转换规则,允许转换克隆结果对象属性的命名风格。
  • 可自定义的克隆算法:支持自定义克隆算法,通过注册钩子函数定制对特定类型的克隆算法。
  • Vue.js 反应性支持:兼容 Vue.js 的反应性系统,只克隆可枚举属性。
  • 100% 测试覆盖率:该库的每一行代码、每个分支和每个函数都经过全面测试,确保在不同使用场景下的可靠行为。

目录

安装

此函数库依赖naming-styletype-detecttypeinfo函数库,因此需要先安装它们。

通过 npm 安装:

npm install @babel/runtime @qubit-ltd/naming-style @qubit-ltd/type-detect @qubit-ltd/typeinfo @qubit-ltd/clone

或者通过 yarn 安装

yarn add @babel/runtime @qubit-ltd/naming-style @qubit-ltd/type-detect @qubit-ltd/typeinfo @qubit-ltd/clone

使用方法

class Credential {
  type = '';
  number = '';
}
class Person {
  name = '';
  age = 0;
  credential = new Credential();
}
const obj2 = new Person();
obj2.name = 'Bill Gates';
obj2.age = 30;
obj2.credential.type = 'PASSWORD';
obj2.credential.number = '111111'
const copy2 = clone(obj2);
expect(copy2).toEqual(obj2);
expect(copy2).not.toBe(obj2);
expect(copy2).toBeInstanceOf(Person);
expect(copy2.credential).toBeInstanceOf(Credential);

项目结构

项目的组织结构如下:

  • src/:包含源代码
    • clone.js:深度克隆对象的主函数
    • default-clone-options.js:克隆算法的默认选项
    • register-clone-hook.js:注册自定义克隆钩子的函数
    • unregister-clone-hook.js:注销自定义克隆钩子的函数
    • impl/:不同类型克隆算法的实现
      • clone-array.js:克隆数组的实现
      • clone-buffer.js:克隆缓冲区对象的实现
      • clone-copy-constructable-object.js:克隆具有复制构造函数的对象的实现
      • clone-customized-object.js:克隆自定义对象的实现
      • clone-data-view.js:克隆 DataView 对象的实现
      • clone-error.js:克隆 Error 对象的实现
      • clone-hooks.js:克隆钩子的管理
      • clone-impl.js:克隆算法的核心实现
      • clone-map.js:克隆 Map 对象的实现
      • clone-object-impl.js:克隆普通对象的实现
      • clone-primitive-wrapper-object.js:克隆原始包装对象的实现
      • clone-promise.js:克隆 Promise 对象的实现
      • clone-set.js:克隆 Set 对象的实现
      • clone-typed-array.js:克隆 TypedArray 对象的实现
      • copy-properties.js:用于在对象之间复制属性的工具函数
      • get-target-key.js:重命名属性时获取目标键的工具函数
      • is-empty.js:检查值是否为空的工具函数
  • test/:包含所有功能的综合测试套件
  • dist/:包含构建的分发文件

API 文档

clone(source, [options])

深度克隆一个值或对象。

  • source: any - 要克隆的值或对象。
  • options: object - 克隆算法的选项对象。可能的选项包括:
    • includeAccessor: boolean:若为 true,将克隆属性的访问器(即 getter 和 setter)。 默认为 false
    • excludeReadonly: boolean:若为 true,将不克隆只读属性。默认为 false
    • includeNonEnumerable: boolean:若为 true,将克隆非枚举属性。默认为 false
    • includeNonConfigurable: boolean:若为 true,将克隆非可配置属性。默认为 false
    • convertNaming: boolean - 若为 true,克隆算法将根据指定的命名风格转换目标对象的属性名称。 此选项的默认值为 false
    • sourceNamingStyle: string | NamingStyle, 源对象的命名样式。该选项仅在 convertNaming 选项设置为 true 时有效。该选项的值可以是表示命名样式名称的字符串,也可以是一个 NamingStyle 实例。默认值为 LOWER_CAMEL
    • targetNamingStyle: string | NamingStyle, 目标对象(即克隆结果对象)的命名样式。 该选项仅在 convertNaming 选项设置为 true 时有效。该选项的值可以是表示命名样式名称 的字符串,也可以是一个NamingStyle 实例。默认值为 LOWER_CAMEL
    • pojo: boolean - 如果此选项设置为 true,克隆算法将把源对象转换为普通的 JavaScript 对象 (POJO)。此选项的默认值为false
    • removeEmptyFields: boolean - 如果此选项设置为 true,克隆算法将在克隆之前递归地移 除源对象的空字段。所谓空字段,是指值为nullundefined,空字符串,空数组,空集合的字段。 此选项的默认值为false
    • disableHooks: boolean - 如果此选项设置为 true,克隆算法将禁用克隆钩子函数。 此选项的默认值为 false
    • useToJSON: boolean - 如果该选项设置为 true,并且源对象具有 toJSON() 方法, 克隆算法将使用源对象的 toJSON() 方法作为克隆的结果。此选项的默认值为 false
    • skipRootToJSON: boolean - 如果该选项和 useToJSON 选项都设置为 true,并且源 对象具有 toJSON() 方法,克隆算法只有在源对象不是克隆过程的根对象时,才会使用 toJSON() 方法的结果作为克隆结果。当使用 clone() 函数实现类或对象的 toJSON() 方法时,该选项 非常有用,因为它可以避免无限递归。此选项的默认值为 false

克隆函数支持对 JavaScript 内置对象的克隆,包括但不限于 primitive 类型、数组、MapSet等。 具体的支持如下:

  • primitive 类型 undefinednullbooleannumberstringsymbolbigint:直接返回原始值;
  • 函数类型:要完整实现对函数的 clone 会带来很多技术上的麻烦,因此本函数对函数类型的值不做克隆,直接返回原始函数;
  • 对象类型:分为 JavaScript 内置对象和用户对象两种情况:
    • 普通非容器型内置对象:会返回一个新的对象,和原始对象完全一致,包括用户增加在原始对象上的自定义属性, 也会一起被深度克隆;
    • 内置容器对象,包括ArrayMapSetInt8ArrayBigUint64Array等:会克隆容器对象 本身,同时深度克隆容器对象中的元素;
    • 弱引用对象,包括:WeakMapWeakSetWeakRef等:不可被克隆,直接返回该对象本身;
    • Buffer对象,包括 ArrayBufferSharedArrayBuffer等:会克隆容器对象本身,同时克隆容器对象中的数据;
    • Promise对象:会克隆一个新的 Promise 对象,包括用户增加在原始对象上的自定义属性;
    • Intl 内置对象的子对象,包括Intl.CollatorIntl.DateTimeFormat等:不可被克隆,直接返回该对象本身;
    • Iterator 对象,包括ArrayIteratorMapIteratorSetIterator等:不可被克隆,直接返回该对象本身;
    • 表示函数参数的 arguments 对象:不可被克隆,直接返回该对象本身;
    • FinalizationRegistry 对象:不可被克隆,直接返回该对象本身;
    • 生成器对象,包括 GeneratorAsyncGenerator:不可被克隆,因此直接返回该对象本身;
    • 全局对象:不可被克隆,直接返回该对象本身;
    • 其他用户自定义对象:深度克隆该对象所有属性,并保持被克隆对象的原型。是否克隆只读属性、不可枚举属性、不可配置属性、 访问器属性等,取决于调用 clone() 函数的第二个克隆算法选项参数。

registerCloneHook(hook)

注册一个自定义对象克隆的钩子函数。

  • hook: function - 钩子函数,其形式应为:
    function cloneHook(info, obj, options) {};
    
    其中:
    • info: object:待克隆对象的类型信息,由 typeInfo() 函数提供。
    • obj: object:待克隆的对象,保证非空。
    • options: object:克隆算法的选项。

unregisterCloneHook(hook)

注销一个自定义对象克隆的钩子函数。

  • hook: function - 要注销的钩子函数,其形式和参数与 registerCloneHook() 相同。

cloneImpl(source, depth, options, cache)

实现了具体的clone 算法。这是一个内部函数,可用于实现自定义的克隆钩子函数。

  • source: any - 待克隆的对象。
  • depth: number - 当前克隆的深度。根对象的深度为 0。
  • options: object - 克隆算法的选项。
  • cache: WeakMap - 用于防止循环引用的对象缓存。

copyProperties(source, target, depth, options, cache)

将源对象的属性复制到目标对象。这是一个内部函数,可用于实现自定义的克隆钩子函数。

  • source: any - 源对象。
  • target: any - 目标对象。
  • depth: number - 当前克隆的深度。根对象的深度为 0。
  • options: object - 克隆算法的选项。
  • cache: WeakMap - 用于防止循环引用的对象缓存。

示例

深度克隆对象

下面的代码例子展示了如何深度克隆一个对象,可以是简单对象,也可以是自定义类的实例。

import clone from '@qubit-ltd/clone';

const obj1 = { a: 1, b: { c: 2 } };
const copy1 = clone(obj1);
expect(copy1).toEqual(obj1);
expect(copy1).not.toBe(obj1);

class Credential {
  type = '';
  number = '';
}
class Person {
  name = '';
  age = 0;
  credential = new Credential();
}
const obj2 = new Person();
obj2.name = 'Bill Gates';
obj2.age = 30;
obj2.credential.type = 'PASSWORD';
obj2.credential.number = '111111'
const copy2 = clone(obj2);
expect(copy2).toEqual(obj2);
expect(copy2).not.toBe(obj2);
expect(copy2).toBeInstanceOf(Person);
expect(copy2.credential).toBeInstanceOf(Credential);

克隆算法选项

下面的代码例子展示了如何使用自定义的克隆算法选项。具体的选项请参考API 文档

const obj = {
  x: 1,
  y: 2,
  _name: 'obj',
  get z() {
    return this.x + this.y;
  },
  get name() {
    return this._name;
  },
  set name(s) {
    this._name = s;
  },
};
Object.defineProperties(obj, {
  r: {
    value: 'readonly',
    writable: false,
    configurable: true,
    enumerable: true,
  },
});
Object.defineProperties(obj, {
  nc: {
    value: 'non-configurable',
    writable: true,
    configurable: false,
    enumerable: true,
  },
});
Object.defineProperties(obj, {
  ne: {
    value: 'non-enumerable',
    writable: true,
    configurable: true,
    enumerable: false,
  },
});

// clone with default options
const copy1 = clone(obj);
expect(copy1.x).toBe(1);
expect(copy1.y).toBe(2);
expect(copy1.r).toBe('readonly');
expect(copy1.z).toBe(3);
expect(typeof copy1.z).toBe('number');
expect(copy1.name).toBe('obj');
expect(typeof copy1.name).toBe('string');
expect(copy1._name).toBe('obj');
expect(typeof copy1._name).toBe('string');
expect('nc' in copy1).toBe(false);
expect('ne' in copy1).toBe(false);

// clone with customized options
const options = {
  includeAccessor: true,
  excludeReadonly: true,
  includeNonEnumerable: true,
  includeNonConfigurable: false,
};
const copy2 = clone(obj, options);
expect(copy2.x).toBe(1);
expect(copy2.y).toBe(2);
expect('r' in copy2).toBe(false);
expect(copy2.z).toBe(3);
expect(copy2._name).toBe('obj');
expect(copy2.name).toBe('obj');
const zd = Object.getOwnPropertyDescriptor(copy2, 'z');
expect(typeof zd.get).toBe('function');
expect(typeof zd.set).toBe('undefined');
expect('value' in zd).toBe(false);
const nd = Object.getOwnPropertyDescriptor(copy2, 'name');
expect(typeof nd.get).toBe('function');
expect(typeof nd.set).toBe('function');
expect('value' in nd).toBe(false);
copy2.name = 'xxx';
expect(copy2.name).toBe('xxx');
expect(copy2._name).toBe('xxx');
expect('ne' in copy2).toBe(true);
expect(copy2.ne).toBe('non-enumerable');
expect('nc' in copy2).toBe(false);

带命名转换的克隆

以下代码示例演示了如何使用自定义命名转换规则进行克隆。具体选项请参考 API 文档

import clone from '@qubit-ltd/clone';

class Credential {
  type = '';
  number = '';
}
class Person {
  name = '';
  age = 0;
  credential = new Credential();
}
const person = new Person();
person.name = 'Bill Gates';
person.age = 30;
person.credential.type = 'PASSWORD';
person.credential.number = '111111';
const copy2 = clone(person);
expect(copy2).toEqual(person);
expect(copy2).not.toBe(person);
expect(copy2).toBeInstanceOf(Person);
expect(copy2.credential).toBeInstanceOf(Credential);

const obj = {
  first_field: 'first-field',
  second_field: {
    first_child_field: 'first-child-field',
    second_child_field: {
      the_person: person,
    },
  }
};
const copy = clone(obj, {
  convertNaming: true,
  sourceNamingStyle: 'lower-underscore',
  targetNamingStyle: 'lower_camel',
});
expect(copy).toBeInstanceOf(Object);
expect(copy.firstField).toBe(obj.first_field);
expect(copy.secondField).toBeInstanceOf(Object);
expect(copy.secondField.firstChildField).toBe(obj.second_field.first_child_field);
expect(copy.secondField.secondChildField).toBeInstanceOf(Object);
expect(copy.secondField.secondChildField.thePerson).toBeInstanceOf(Person);
expect(copy.secondField.secondChildField.thePerson).toEqual(person);
expect(copy.secondField.secondChildField.thePerson).not.toBe(person);

请注意,命名转换样式可以通过字符串或 NamingStyle 实例指定。如果通过字符串指定,则字符串不 区分大小写,并且字符 '-''_'; 被视为相同。有关更多详细信息,请参阅 NamingStyle.of() 函数。

定制克隆行为

import { registerCloneHook, clone } from '@qubit-ltd/clone';

function customCloneHook(info, obj, options) {
  if (info.constructor === MyCustomClass) {
    const result = new MyCustomClass();
    // implements the customized clone algorithm
    return result;
  }
  return null;
}

registerCloneHook(customCloneHook);

const original = {
  name: 'original',
  data: new MyCustomClass(),
};
const cloned = clone(original);

unregisterCloneHook(customCloneHook);

克隆带循环引用的复杂结构

克隆函数可以处理带有循环引用的复杂对象,防止无限递归:

import clone from '@qubit-ltd/clone';

// 创建一个带有循环引用的对象
const original = {
  name: 'original',
  nested: {
    data: 42
  }
};
// 创建循环引用
original.self = original;
original.nested.parent = original;

// 安全地克隆
const cloned = clone(original);

// 验证循环引用被正确维护
expect(cloned.self).toBe(cloned); // 不是指向原始对象,而是指向克隆对象
expect(cloned.nested.parent).toBe(cloned);
expect(cloned).not.toBe(original);
expect(cloned.nested).not.toBe(original.nested);

与 Vue.js 一起使用

克隆函数与 Vue.js 的反应性系统兼容:

import { reactive } from 'vue';
import clone from '@qubit-ltd/clone';

// 创建一个响应式对象
const original = reactive({
  count: 0,
  items: [1, 2, 3],
  nested: {
    value: 'test'
  }
});

// 克隆它
const cloned = clone(original);

// 克隆对象不是响应式的,但具有所有相同的值
console.log(cloned.count); // 0
console.log(cloned.items); // [1, 2, 3]
console.log(cloned.nested.value); // 'test'

// 如果需要,可以将克隆对象再次转为响应式
const reactiveClone = reactive(cloned);

测试覆盖率

本库具有100%的测试覆盖率,涵盖所有指标:

  • 语句覆盖率: 100% (135/135)
  • 分支覆盖率: 100% (98/98)
  • 函数覆盖率: 100% (20/20)
  • 行覆盖率: 100% (135/135)

测试套件包含超过34个测试套件,共347个独立测试用例,覆盖了库的所有功能,包括边缘情况、不同对象类型和各种配置选项。

要运行测试并检查覆盖率:

yarn test --coverage

许可证

clone 在 Apache 2.0 许可下分发。有关更多详情,请参阅 LICENSE 文件。

贡献方式

如果您发现任何问题或有改进建议,请随时在GitHub仓库中提出问题或提交拉取请求。

在贡献此项目时,请确保:

  1. 所有测试都通过,并保持100%的覆盖率
  2. 新功能或变更都有适当的文档
  3. 代码遵循现有的风格约定
  4. 提交消息清晰且具有描述性

贡献者