Skip to content

Commit

Permalink
feat: try native structuredClone in cloneDeep
Browse files Browse the repository at this point in the history
resolves lodash#5833
  • Loading branch information
lewxdev committed Apr 29, 2024
1 parent a67a085 commit 878017c
Show file tree
Hide file tree
Showing 3 changed files with 682 additions and 662 deletions.
27 changes: 24 additions & 3 deletions src/cloneDeep.ts
Expand Up @@ -6,11 +6,18 @@ const CLONE_SYMBOLS_FLAG = 4;

/**
* This method is like `clone` except that it recursively clones `value`.
* Object inheritance is preserved.
* Object inheritance is preserved. The method will attempt to use the native
* [`structuredClone`](https://developer.mozilla.org/docs/Web/API/structuredClone)
* function, if `value` [is supported](https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types)
* (and the native implementation is available). Otherwise it will fallback to a
* custom implementation.
*
* @since 1.0.0
* @category Lang
* @param {*} value The value to recursively clone.
* @param {boolean} [skipNativeCheck]
* Skip the native check and use the custom implementation. This is useful when
* `value` is known to be incompatible `structuredClone`.
* @returns {*} Returns the deep cloned value.
* @see clone
* @example
Expand All @@ -20,9 +27,23 @@ const CLONE_SYMBOLS_FLAG = 4;
* const deep = cloneDeep(objects)
* console.log(deep[0] === objects[0])
* // => false
*
* // The `skipNativeCheck` flag
* const unsupportedNativeObject = { fn: () => 'a' };
*
* const deep = cloneDeep(unsupportedNativeObject, true);
* console.log(deep === unsupportedNativeObject);
* // => false
*/
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
function cloneDeep(value, skipNativeCheck) {
try {
if (!skipNativeCheck && structuredClone) {
return structuredClone(value);
}
throw new Error('Unsupported structured clone');
} catch {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}
}

export default cloneDeep;
26 changes: 17 additions & 9 deletions test/clone-methods.spec.js
Expand Up @@ -86,7 +86,7 @@ xdescribe('clone methods', function () {
const actual = _.clone(array);

expect(actual).toEqual(array);
expect(actual !== array && actual[0] === array[0])
expect(actual !== array && actual[0] === array[0]);
});

it('`_.cloneDeep` should deep clone objects with circular references', () => {
Expand Down Expand Up @@ -117,14 +117,22 @@ xdescribe('clone methods', function () {
assert.notStrictEqual(actual, cyclical[`v${LARGE_ARRAY_SIZE - 1}`]);
});

it('`_.cloneDeep` should accept the `skipNativeCheck` flag', () => {
const object = { primitive: 'a', fn: () => 'b' };
const actual = cloneDeep(object, true);

expect(actual).not.toEqual(object);
expect(actual.primitive).not.toEqual(object.primitive);
});

it('`_.cloneDeepWith` should provide `stack` to `customizer`', () => {
let actual;

cloneDeepWith({ a: 1 }, function () {
actual = last(arguments);
});

expect(isNpm ? actual.constructor.name === 'Stack' : actual instanceof mapCaches.Stack)
expect(isNpm ? actual.constructor.name === 'Stack' : actual instanceof mapCaches.Stack);
});

lodashStable.each(['clone', 'cloneDeep'], (methodName) => {
Expand All @@ -134,7 +142,7 @@ xdescribe('clone methods', function () {
lodashStable.forOwn(objects, (object, kind) => {
it(`\`_.${methodName}\` should clone ${kind}`, () => {
const actual = func(object);
expect(lodashStable.isEqual(actual, object))
expect(lodashStable.isEqual(actual, object));

if (lodashStable.isObject(object)) {
assert.notStrictEqual(actual, object);
Expand Down Expand Up @@ -198,23 +206,23 @@ xdescribe('clone methods', function () {
it(`\`_.${methodName}\` should clone prototype objects`, () => {
const actual = func(Foo.prototype);

expect((actual instanceof Foo)).toBe(false)
expect(actual instanceof Foo).toBe(false);
expect(actual).toEqual({ b: 1 });
});

it(`\`_.${methodName}\` should set the \`[[Prototype]]\` of a clone`, () => {
expect(func(new Foo()) instanceof Foo)
expect(func(new Foo()) instanceof Foo);
});

it(`\`_.${methodName}\` should set the \`[[Prototype]]\` of a clone even when the \`constructor\` is incorrect`, () => {
Foo.prototype.constructor = Object;
expect(func(new Foo()) instanceof Foo)
expect(func(new Foo()) instanceof Foo);
Foo.prototype.constructor = Foo;
});

it(`\`_.${methodName}\` should ensure \`value\` constructor is a function before using its \`[[Prototype]]\``, () => {
Foo.prototype.constructor = null;
expect((func(new Foo()) instanceof Foo)).toBe(false)
expect(func(new Foo()) instanceof Foo).toBe(false);
Foo.prototype.constructor = Foo;
});

Expand Down Expand Up @@ -297,7 +305,7 @@ xdescribe('clone methods', function () {
try {
expect(func(element)).toEqual({});
} catch (e) {
expect(false, e.message)
expect(false, e.message);
}
}
});
Expand Down Expand Up @@ -417,7 +425,7 @@ xdescribe('clone methods', function () {
argsList.push(args);
});

expect(argsList, isDeep ? [[object], [1, 'a').toEqual(object]] : [[object]]);
// expect(argsList, isDeep ? [[object], [1, 'a').toEqual(object]] : [[object]]);
});

it(`\`_.${methodName}\` should handle cloning when \`customizer\` returns \`undefined\``, () => {
Expand Down

0 comments on commit 878017c

Please sign in to comment.