From d266fb26091fef252d7351c0610cab465681e1b1 Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Tue, 2 Aug 2022 14:44:57 +0200 Subject: [PATCH 01/12] tests: add tests that test the new circular ref cloning --- test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test.js b/test.js index a93725e..2664de7 100644 --- a/test.js +++ b/test.js @@ -43,6 +43,25 @@ describe('cloneDeep()', function() { assert.notDeepEqual(arr1, arr2); }); + it('should deeply clone object with circular references', function() { + const one = {a: 'b'}; + one.cyclic = one; + const two = clone(one); + two.c = 'd'; + assert.notDeepEqual(one, two); + // assert.deepEqual(two, two.cyclic); // todo find out what to use here + }); + + it('should deeply clone arrays with circular references', function() { + const one = {a: 'b'}; + const arr1 = [one]; + arr1.push(arr1) + const arr2 = clone(arr1); + one.c = 'd'; + assert.notDeepEqual(arr1, arr2); + // assert.equal(arr1, arr1[1]); todo find what to use here + }); + it('should deeply clone Map', function() { const a = new Map([[1, 5]]); const b = clone(a); From 7e8d955ea0be8caa2253c19e026b872b24e5dda7 Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Tue, 2 Aug 2022 14:45:18 +0200 Subject: [PATCH 02/12] feat: add new circular ref cloning for objects --- index.js | 21 +++++++++++++++++---- package.json | 5 ++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 9133c08..ab3d76f 100644 --- a/index.js +++ b/index.js @@ -7,8 +7,12 @@ const clone = require('shallow-clone'); const typeOf = require('kind-of'); const isPlainObject = require('is-plain-object'); +const whatsCircular = require('whats-circular'); +const includes = require('lodash.includes'); +const get = require('lodash.get'); -function cloneDeep(val, instanceClone) { + +function cloneDeep (val, instanceClone) { switch (typeOf(val)) { case 'object': return cloneObjectDeep(val, instanceClone); @@ -20,21 +24,29 @@ function cloneDeep(val, instanceClone) { } } -function cloneObjectDeep(val, instanceClone) { + +function cloneObjectDeep (val, instanceClone) { if (typeof instanceClone === 'function') { return instanceClone(val); } if (instanceClone || isPlainObject(val)) { const res = new val.constructor(); + const circulars = (whatsCircular(val) ?? []).map(c => get(val, c)); + for (let key in val) { - res[key] = cloneDeep(val[key], instanceClone); + if (includes(circulars, val[key])) { + res[key] = clone(val[key]); + } else { + res[key] = cloneDeep(val[key], instanceClone); + } } return res; } return val; } -function cloneArrayDeep(val, instanceClone) { + +function cloneArrayDeep (val, instanceClone) { const res = new val.constructor(val.length); for (let i = 0; i < val.length; i++) { res[i] = cloneDeep(val[i], instanceClone); @@ -42,6 +54,7 @@ function cloneArrayDeep(val, instanceClone) { return res; } + /** * Expose `cloneDeep` */ diff --git a/package.json b/package.json index 7b494ea..3fa2068 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,10 @@ "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "lodash.get": "^4.4.2", + "lodash.includes": "^4.3.0", + "shallow-clone": "^3.0.0", + "whats-circular": "^2.0.2" }, "devDependencies": { "gulp-format-md": "^2.0.0", From 81b28bd17522ca20de6c6db1732492443ff4bd67 Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Tue, 2 Aug 2022 14:46:54 +0200 Subject: [PATCH 03/12] feat: add new circular ref cloning for arrays --- index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ab3d76f..689aa51 100644 --- a/index.js +++ b/index.js @@ -48,8 +48,14 @@ function cloneObjectDeep (val, instanceClone) { function cloneArrayDeep (val, instanceClone) { const res = new val.constructor(val.length); + const circulars = (whatsCircular(val) ?? []).map(c => get(val, c)); + for (let i = 0; i < val.length; i++) { - res[i] = cloneDeep(val[i], instanceClone); + if (includes(circulars, val[i])) { + res[i] = clone(val[i]); + } else { + res[i] = cloneDeep(val[i], instanceClone); + } } return res; } From c8d9ce3ba1de4f1ba3f91d9d45e6eb38ba981bbd Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Tue, 2 Aug 2022 14:56:57 +0200 Subject: [PATCH 04/12] docs: add new functionality to docs --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 943a435..0f472d3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # clone-deep [![NPM version](https://img.shields.io/npm/v/clone-deep.svg?style=flat)](https://www.npmjs.com/package/clone-deep) [![NPM monthly downloads](https://img.shields.io/npm/dm/clone-deep.svg?style=flat)](https://npmjs.org/package/clone-deep) [![NPM total downloads](https://img.shields.io/npm/dt/clone-deep.svg?style=flat)](https://npmjs.org/package/clone-deep) [![Linux Build Status](https://img.shields.io/travis/jonschlinkert/clone-deep.svg?style=flat&label=Travis)](https://travis-ci.org/jonschlinkert/clone-deep) -> Recursively (deep) clone JavaScript native types, like Object, Array, RegExp, Date as well as primitives. +> Recursively (deep) clone JavaScript native types, like Object, Array, RegExp, Date as well as primitives. Supports circular objects clonning. Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. @@ -19,6 +19,7 @@ const cloneDeep = require('clone-deep'); let obj = { a: 'b' }; let arr = [obj]; + let copy = cloneDeep(arr); obj.c = 'd'; @@ -27,6 +28,12 @@ console.log(copy); console.log(arr); //=> [{ a: 'b', c: 'd' }] + +obj.e = obj; +copy = cloneDeep(arr); + +console.log(copy); +//=> [{ a: 'b', c: 'd', e: {...} }] // handles circular arrays and objects cloning ``` ## Heads up! @@ -84,9 +91,10 @@ You might also be interested in these projects: ### Contributors | **Commits** | **Contributor** | -| --- | --- | +| -- | --- | | 46 | [jonschlinkert](https://github.com/jonschlinkert) | -| 2 | [yujunlong2000](https://github.com/yujunlong2000) | +| 2 | [yujunlong2000](https://github.com/yujunlong2000) | +| 5 | [emahuni](https://github.com/emahuni) | ### Author From 2fdb29f89afbe4d62bee976633708ff1db01b82d Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Tue, 2 Aug 2022 15:18:46 +0200 Subject: [PATCH 05/12] fix: don't use null coalescing operator --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 689aa51..93c22eb 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,7 @@ function cloneObjectDeep (val, instanceClone) { } if (instanceClone || isPlainObject(val)) { const res = new val.constructor(); - const circulars = (whatsCircular(val) ?? []).map(c => get(val, c)); + const circulars = (whatsCircular(val) || []).map(c => get(val, c)); for (let key in val) { if (includes(circulars, val[key])) { @@ -48,7 +48,7 @@ function cloneObjectDeep (val, instanceClone) { function cloneArrayDeep (val, instanceClone) { const res = new val.constructor(val.length); - const circulars = (whatsCircular(val) ?? []).map(c => get(val, c)); + const circulars = (whatsCircular(val) || []).map(c => get(val, c)); for (let i = 0; i < val.length; i++) { if (includes(circulars, val[i])) { From 7030960175d991e91872e88474599641deacf526 Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Tue, 2 Aug 2022 18:59:24 +0200 Subject: [PATCH 06/12] fix: fix object cloning with circular refs (pass test) --- index.js | 48 +++++++++++++++++++++++------ package.json | 3 ++ test.js | 87 +++++++++++++++++++++++++++++----------------------- 3 files changed, 91 insertions(+), 47 deletions(-) diff --git a/index.js b/index.js index 93c22eb..bf4f313 100644 --- a/index.js +++ b/index.js @@ -9,15 +9,38 @@ const typeOf = require('kind-of'); const isPlainObject = require('is-plain-object'); const whatsCircular = require('whats-circular'); const includes = require('lodash.includes'); +const find = require('lodash.find'); const get = require('lodash.get'); +const set = require('lodash.set'); function cloneDeep (val, instanceClone) { + const circularsPaths = whatsCircular(val); + const circulars = circularsPaths !== undefined ? + circularsPaths.map(c => ({ val: get(val, c), path: c, parentPath: undefined })) : + undefined; + + let cloned = _cloneDeep(val, instanceClone, circulars, circularsPaths, []); + + if (Array.isArray(circulars)) { + circulars.forEach((c) => { + const pointer = get(cloned, c.parentPath); + set(cloned, c.path, pointer); + }); + } + + return cloned; +} + + +function _cloneDeep (val, instanceClone, circulars, circularsPaths, parentPath) { switch (typeOf(val)) { case 'object': - return cloneObjectDeep(val, instanceClone); + return cloneObjectDeep(val, instanceClone, circulars, circularsPaths, parentPath); + break; case 'array': - return cloneArrayDeep(val, instanceClone); + return cloneArrayDeep(val, instanceClone, circulars, circularsPaths, parentPath); + break; default: { return clone(val); } @@ -25,19 +48,26 @@ function cloneDeep (val, instanceClone) { } -function cloneObjectDeep (val, instanceClone) { +function cloneObjectDeep (val, instanceClone, circulars, circularsPaths, parentPath) { if (typeof instanceClone === 'function') { return instanceClone(val); } if (instanceClone || isPlainObject(val)) { const res = new val.constructor(); - const circulars = (whatsCircular(val) || []).map(c => get(val, c)); for (let key in val) { - if (includes(circulars, val[key])) { - res[key] = clone(val[key]); - } else { - res[key] = cloneDeep(val[key], instanceClone); + const keyPath = parentPath.concat([key]); + // check if value of val[key] is in circulars (is this not a parent of a circular) and ensure we didn't already assign a parent path to it? + let circ, goDeeper = true; + if ((circ = find(circulars, (c) => !c.parentPath && c.val === val[key], 0))) { + circ.parentPath = keyPath; + } else if ((circ = find(circulars, { path: keyPath }, 0))) { + goDeeper = false; + // res[key] = get(cloned, circ.path); // no access to main obj + } + + if (goDeeper) { + res[key] = _cloneDeep(val[key], instanceClone, circulars, circularsPaths, keyPath); } } return res; @@ -54,7 +84,7 @@ function cloneArrayDeep (val, instanceClone) { if (includes(circulars, val[i])) { res[i] = clone(val[i]); } else { - res[i] = cloneDeep(val[i], instanceClone); + res[i] = _cloneDeep(val[i], instanceClone); } } return res; diff --git a/package.json b/package.json index 3fa2068..c3da134 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,11 @@ "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", + "lodash.find": "^4.6.0", + "lodash.findindex": "^4.6.0", "lodash.get": "^4.4.2", "lodash.includes": "^4.3.0", + "lodash.set": "^4.3.2", "shallow-clone": "^3.0.0", "whats-circular": "^2.0.2" }, diff --git a/test.js b/test.js index 2664de7..36a884f 100644 --- a/test.js +++ b/test.js @@ -4,8 +4,8 @@ require('mocha'); const assert = require('assert'); const clone = require('./'); -describe('cloneDeep()', function() { - it('should clone arrays', function() { +describe('cloneDeep()', function () { + it('should clone arrays', function () { assert.deepEqual(clone(['alpha', 'beta', 'gamma']), ['alpha', 'beta', 'gamma']); assert.deepEqual(clone([1, 2, 3]), [1, 2, 3]); @@ -15,12 +15,12 @@ describe('cloneDeep()', function() { assert.deepEqual(b, a); assert.deepEqual(b[0], a[0]); - const val = [0, 'a', {}, [{}], [function() {}], function() {}]; + const val = [0, 'a', {}, [{}], [function () {}], function () {}]; assert.deepEqual(clone(val), val); }); - it('should deeply clone an array', function() { - const fixture = [[{a: 'b'}], [{a: 'b'}]]; + it('should deeply clone an array', function () { + const fixture = [[{ a: 'b' }], [{ a: 'b' }]]; const result = clone(fixture); assert(fixture !== result); assert(fixture[0] !== result[0]); @@ -28,83 +28,90 @@ describe('cloneDeep()', function() { assert.deepEqual(fixture, result); }); - it('should deeply clone object', function() { - const one = {a: 'b'}; + it('should deeply clone object', function () { + const one = { a: 'b' }; const two = clone(one); two.c = 'd'; assert.notDeepEqual(one, two); }); - it('should deeply clone arrays', function() { - const one = {a: 'b'}; + it('should deeply clone arrays', function () { + const one = { a: 'b' }; const arr1 = [one]; const arr2 = clone(arr1); one.c = 'd'; assert.notDeepEqual(arr1, arr2); }); - it('should deeply clone object with circular references', function() { - const one = {a: 'b'}; - one.cyclic = one; + it.only('should deeply clone object with circular references', function () { + const one = { a: false, b: { c: '3' } }; + one.b.cyclic = one.b; const two = clone(one); - two.c = 'd'; + two.b.cyclic.c = 'e'; assert.notDeepEqual(one, two); - // assert.deepEqual(two, two.cyclic); // todo find out what to use here + assert.equal(two.b, two.b.cyclic); + assert.notEqual(one.b.cyclic.c, two.b.cyclic.c); }); - it('should deeply clone arrays with circular references', function() { - const one = {a: 'b'}; + it('should deeply clone arrays with circular references', function () { + const one = { a: 'b' }; const arr1 = [one]; - arr1.push(arr1) + arr1.push(arr1); const arr2 = clone(arr1); one.c = 'd'; assert.notDeepEqual(arr1, arr2); // assert.equal(arr1, arr1[1]); todo find what to use here }); - it('should deeply clone Map', function() { + it('should deeply clone Map', function () { const a = new Map([[1, 5]]); const b = clone(a); a.set(2, 4); assert.notDeepEqual(Array.from(a), Array.from(b)); }); - it('should deeply clone Set', function() { + it('should deeply clone Set', function () { const a = new Set([2, 1, 3]); const b = clone(a); a.add(8); assert.notDeepEqual(Array.from(a), Array.from(b)); }); - it('should return primitives', function() { + it('should return primitives', function () { assert.equal(clone(0), 0); assert.equal(clone('foo'), 'foo'); }); - it('should clone a regex', function() { + it('should clone a regex', function () { assert.deepEqual(clone(/foo/g), /foo/g); }); - it('should clone objects', function() { - assert.deepEqual(clone({a: 1, b: 2, c: 3 }), {a: 1, b: 2, c: 3 }); + it('should clone objects', function () { + assert.deepEqual(clone({ a: 1, b: 2, c: 3 }), { a: 1, b: 2, c: 3 }); }); - it('should deeply clone objects', function() { - assert.deepEqual(clone({a: {a: 1, b: 2, c: 3 }, b: {a: 1, b: 2, c: 3 }, c: {a: 1, b: 2, c: 3 } }), {a: {a: 1, b: 2, c: 3 }, b: {a: 1, b: 2, c: 3 }, c: {a: 1, b: 2, c: 3 } }); + it('should deeply clone objects', function () { + assert.deepEqual(clone({ a: { a: 1, b: 2, c: 3 }, b: { a: 1, b: 2, c: 3 }, c: { a: 1, b: 2, c: 3 } }), { + a: { a: 1, b: 2, c: 3 }, + b: { a: 1, b: 2, c: 3 }, + c: { a: 1, b: 2, c: 3 }, + }); }); - it('should deep clone instances with instanceClone true', function() { - function A(x, y, z) { + it('should deep clone instances with instanceClone true', function () { + function A (x, y, z) { this.x = x; this.y = y; this.z = z; } - function B(x) { + + function B (x) { this.x = x; } - const a = new A({x: 11, y: 12, z: () => 'z'}, new B(2), 7); + + const a = new A({ x: 11, y: 12, z: () => 'z' }, new B(2), 7); const b = clone(a, true); assert.deepEqual(a, b); @@ -116,18 +123,20 @@ describe('cloneDeep()', function() { assert.notEqual(a.y.x, b.y.x, 'Nested property of original object not expected to be changed'); }); - it('should not deep clone instances', function() { - function A(x, y, z) { + it('should not deep clone instances', function () { + function A (x, y, z) { this.x = x; this.y = y; this.z = z; } - function B(x) { + + function B (x) { this.x = x; } - const a = new A({x: 11, y: 12, z: () => 'z'}, new B(2), 7); + + const a = new A({ x: 11, y: 12, z: () => 'z' }, new B(2), 7); const b = clone(a); assert.deepEqual(a, b); @@ -139,19 +148,21 @@ describe('cloneDeep()', function() { assert.equal(a.y.x, b.y.x); }); - it('should deep clone instances with instanceClone self defined', function() { - function A(x, y, z) { + it('should deep clone instances with instanceClone self defined', function () { + function A (x, y, z) { this.x = x; this.y = y; this.z = z; } - function B(x) { + + function B (x) { this.x = x; } - const a = new A({x: 11, y: 12, z: () => 'z'}, new B(2), 7); - const b = clone(a, function(val){ + + const a = new A({ x: 11, y: 12, z: () => 'z' }, new B(2), 7); + const b = clone(a, function (val) { if (val instanceof A) { const res = new A(); for (const key in val) { From 1f3adf703e8fa0ccf1de5a9d2fd1b85c7c1ae4c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Wed, 3 Aug 2022 06:33:07 +0200 Subject: [PATCH 07/12] perf: combine array and object cloning functions into 1 --- index.js | 30 +++++++----------------------- package.json | 1 + 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index bf4f313..136c3bf 100644 --- a/index.js +++ b/index.js @@ -35,12 +35,9 @@ function cloneDeep (val, instanceClone) { function _cloneDeep (val, instanceClone, circulars, circularsPaths, parentPath) { switch (typeOf(val)) { + case 'array': case 'object': return cloneObjectDeep(val, instanceClone, circulars, circularsPaths, parentPath); - break; - case 'array': - return cloneArrayDeep(val, instanceClone, circulars, circularsPaths, parentPath); - break; default: { return clone(val); } @@ -49,11 +46,12 @@ function _cloneDeep (val, instanceClone, circulars, circularsPaths, parentPath) function cloneObjectDeep (val, instanceClone, circulars, circularsPaths, parentPath) { - if (typeof instanceClone === 'function') { + if (typeof instanceClone === 'function' && !Array.isArray(val)) { return instanceClone(val); } - if (instanceClone || isPlainObject(val)) { - const res = new val.constructor(); + + if (instanceClone || Array.isArray(val) || isPlainObject(val)) { + const res = new val.constructor(Array.isArray(val) ? val.length : undefined); for (let key in val) { const keyPath = parentPath.concat([key]); @@ -63,31 +61,17 @@ function cloneObjectDeep (val, instanceClone, circulars, circularsPaths, parentP circ.parentPath = keyPath; } else if ((circ = find(circulars, { path: keyPath }, 0))) { goDeeper = false; - // res[key] = get(cloned, circ.path); // no access to main obj } if (goDeeper) { res[key] = _cloneDeep(val[key], instanceClone, circulars, circularsPaths, keyPath); } } + return res; } - return val; -} - -function cloneArrayDeep (val, instanceClone) { - const res = new val.constructor(val.length); - const circulars = (whatsCircular(val) || []).map(c => get(val, c)); - - for (let i = 0; i < val.length; i++) { - if (includes(circulars, val[i])) { - res[i] = clone(val[i]); - } else { - res[i] = _cloneDeep(val[i], instanceClone); - } - } - return res; + return val; } diff --git a/package.json b/package.json index c3da134..46d89d2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "lodash.findindex": "^4.6.0", "lodash.get": "^4.4.2", "lodash.includes": "^4.3.0", + "lodash.isequal": "^4.5.0", "lodash.set": "^4.3.2", "shallow-clone": "^3.0.0", "whats-circular": "^2.0.2" From 91abc38e6e18744dd4c2b518d4206dcdd0a8398a Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Wed, 3 Aug 2022 06:33:39 +0200 Subject: [PATCH 08/12] tests: add and correct a few tests --- test.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test.js b/test.js index 36a884f..b78d220 100644 --- a/test.js +++ b/test.js @@ -43,7 +43,7 @@ describe('cloneDeep()', function () { assert.notDeepEqual(arr1, arr2); }); - it.only('should deeply clone object with circular references', function () { + it('should deeply clone object with circular references', function () { const one = { a: false, b: { c: '3' } }; one.b.cyclic = one.b; const two = clone(one); @@ -53,14 +53,25 @@ describe('cloneDeep()', function () { assert.notEqual(one.b.cyclic.c, two.b.cyclic.c); }); - it('should deeply clone arrays with circular references', function () { + it.skip('should deeply clone object with circular references', function () { + const one = { a: false, b: { c: '3' } }; + one.b.cyclic = one; + const two = clone(one); + two.b.cyclic.a = true; + assert.notDeepEqual(one, two); + assert.equal(two, two.b.cyclic); + assert.notEqual(one.b.cyclic.a, two.b.cyclic.a); + }); + + it.only('should deeply clone arrays with circular references', function () { const one = { a: 'b' }; const arr1 = [one]; arr1.push(arr1); const arr2 = clone(arr1); - one.c = 'd'; + one.a = 'c'; assert.notDeepEqual(arr1, arr2); - // assert.equal(arr1, arr1[1]); todo find what to use here + assert.equal(arr2, arr2[1]); + assert.notEqual(arr1[1].a, arr2[1].a); }); it('should deeply clone Map', function () { From 9d24c68ed475ee2c153b929bb803317e66c3fabf Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Wed, 3 Aug 2022 09:31:31 +0200 Subject: [PATCH 09/12] fix: use simpler & faster algorithm, fewer dependencies --- index.js | 77 ++++++++++++++++++++++++++++------------------------ package.json | 7 +---- test.js | 4 +-- 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/index.js b/index.js index 136c3bf..1dd5624 100644 --- a/index.js +++ b/index.js @@ -7,37 +7,29 @@ const clone = require('shallow-clone'); const typeOf = require('kind-of'); const isPlainObject = require('is-plain-object'); -const whatsCircular = require('whats-circular'); -const includes = require('lodash.includes'); -const find = require('lodash.find'); -const get = require('lodash.get'); -const set = require('lodash.set'); +const findIndex = require('lodash.findindex'); function cloneDeep (val, instanceClone) { - const circularsPaths = whatsCircular(val); - const circulars = circularsPaths !== undefined ? - circularsPaths.map(c => ({ val: get(val, c), path: c, parentPath: undefined })) : - undefined; - - let cloned = _cloneDeep(val, instanceClone, circulars, circularsPaths, []); - - if (Array.isArray(circulars)) { - circulars.forEach((c) => { - const pointer = get(cloned, c.parentPath); - set(cloned, c.path, pointer); - }); + // get an instance of the main val into parentsRes first, we need this for checking and setting of clone circular references + // undefined root will skip cloning in cloneObjectDeep and just get the new instance + const parentsRes = [_cloneDeep(val, instanceClone)]; + + // don't repeat if these will result same as parentsRes[0] + if (/array|object/.test(typeOf(val))) { + if (!/function|undefined/.test(typeof instanceClone) || isPlainObject(val)) { + return _cloneDeep(val, instanceClone, parentsRes[0], parentsRes, [val]); + } } - - return cloned; + return parentsRes[0]; } -function _cloneDeep (val, instanceClone, circulars, circularsPaths, parentPath) { +function _cloneDeep (val, instanceClone, root, parentsRes, parentsVal) { switch (typeOf(val)) { case 'array': case 'object': - return cloneObjectDeep(val, instanceClone, circulars, circularsPaths, parentPath); + return cloneObjectDeep(val, instanceClone, root, parentsRes, parentsVal); default: { return clone(val); } @@ -45,26 +37,41 @@ function _cloneDeep (val, instanceClone, circulars, circularsPaths, parentPath) } -function cloneObjectDeep (val, instanceClone, circulars, circularsPaths, parentPath) { +function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { if (typeof instanceClone === 'function' && !Array.isArray(val)) { return instanceClone(val); } if (instanceClone || Array.isArray(val) || isPlainObject(val)) { - const res = new val.constructor(Array.isArray(val) ? val.length : undefined); - - for (let key in val) { - const keyPath = parentPath.concat([key]); - // check if value of val[key] is in circulars (is this not a parent of a circular) and ensure we didn't already assign a parent path to it? - let circ, goDeeper = true; - if ((circ = find(circulars, (c) => !c.parentPath && c.val === val[key], 0))) { - circ.parentPath = keyPath; - } else if ((circ = find(circulars, { path: keyPath }, 0))) { - goDeeper = false; - } + let res; + if (root) { + res = root; // don't directly use root to avoid confusion + } else { + res = new val.constructor(Array.isArray(val) ? val.length : undefined); + } - if (goDeeper) { - res[key] = _cloneDeep(val[key], instanceClone, circulars, circularsPaths, keyPath); + // if root is undefined then this was just a clone object constructor + if (root) { + for (let key in val) { + let circularIndex = findIndex(parentsVal, val[key]); + if (~circularIndex) { + res[key] = parentsRes[circularIndex]; + } else { + if (Array.isArray(val[key]) || isPlainObject(val[key]) || typeof instanceClone === typeof val[key]) { + // this is some kind of object + parentsVal.push(val[key]); + + const keyRoot = _cloneDeep(val[key], instanceClone); // get instance for object at val[key] + res[key] = keyRoot; + parentsRes.push(keyRoot); + + // the following will clone val[key] object on keyRoot/res[key] + _cloneDeep(val[key], instanceClone, keyRoot, parentsRes, parentsVal); + } else { + // this is a scalar property + res[key] = _cloneDeep(val[key], instanceClone, res, parentsRes, parentsVal); + } + } } } diff --git a/package.json b/package.json index 46d89d2..b98eb03 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,9 @@ "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", - "lodash.find": "^4.6.0", "lodash.findindex": "^4.6.0", - "lodash.get": "^4.4.2", - "lodash.includes": "^4.3.0", "lodash.isequal": "^4.5.0", - "lodash.set": "^4.3.2", - "shallow-clone": "^3.0.0", - "whats-circular": "^2.0.2" + "shallow-clone": "^3.0.0" }, "devDependencies": { "gulp-format-md": "^2.0.0", diff --git a/test.js b/test.js index b78d220..7091fa8 100644 --- a/test.js +++ b/test.js @@ -43,7 +43,7 @@ describe('cloneDeep()', function () { assert.notDeepEqual(arr1, arr2); }); - it('should deeply clone object with circular references', function () { + it('should deeply clone object with circular references if pointing to root object', function () { const one = { a: false, b: { c: '3' } }; one.b.cyclic = one.b; const two = clone(one); @@ -53,7 +53,7 @@ describe('cloneDeep()', function () { assert.notEqual(one.b.cyclic.c, two.b.cyclic.c); }); - it.skip('should deeply clone object with circular references', function () { + it('should deeply clone object with circular references if pointing to inner object', function () { const one = { a: false, b: { c: '3' } }; one.b.cyclic = one; const two = clone(one); From 75254c465246e263ba562b56a0a10e98cff8a37a Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Wed, 3 Aug 2022 09:51:42 +0200 Subject: [PATCH 10/12] fix: array cloning --- index.js | 7 ++++--- test.js | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 1dd5624..f3271fe 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const clone = require('shallow-clone'); const typeOf = require('kind-of'); const isPlainObject = require('is-plain-object'); const findIndex = require('lodash.findindex'); +const isEqual = require('lodash.isequal'); function cloneDeep (val, instanceClone) { @@ -17,7 +18,7 @@ function cloneDeep (val, instanceClone) { // don't repeat if these will result same as parentsRes[0] if (/array|object/.test(typeOf(val))) { - if (!/function|undefined/.test(typeof instanceClone) || isPlainObject(val)) { + if (!/function|undefined/.test(typeof instanceClone) || typeof val === 'object') { return _cloneDeep(val, instanceClone, parentsRes[0], parentsRes, [val]); } } @@ -38,7 +39,7 @@ function _cloneDeep (val, instanceClone, root, parentsRes, parentsVal) { function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { - if (typeof instanceClone === 'function' && !Array.isArray(val)) { + if (typeof instanceClone === 'function') { return instanceClone(val); } @@ -53,7 +54,7 @@ function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { // if root is undefined then this was just a clone object constructor if (root) { for (let key in val) { - let circularIndex = findIndex(parentsVal, val[key]); + let circularIndex = findIndex(parentsVal, v => isEqual(v, val[key])); if (~circularIndex) { res[key] = parentsRes[circularIndex]; } else { diff --git a/test.js b/test.js index 7091fa8..b301f92 100644 --- a/test.js +++ b/test.js @@ -63,7 +63,7 @@ describe('cloneDeep()', function () { assert.notEqual(one.b.cyclic.a, two.b.cyclic.a); }); - it.only('should deeply clone arrays with circular references', function () { + it('should deeply clone arrays with circular references', function () { const one = { a: 'b' }; const arr1 = [one]; arr1.push(arr1); @@ -71,7 +71,7 @@ describe('cloneDeep()', function () { one.a = 'c'; assert.notDeepEqual(arr1, arr2); assert.equal(arr2, arr2[1]); - assert.notEqual(arr1[1].a, arr2[1].a); + assert.notEqual(arr1[1][0].a, arr2[1][0].a); }); it('should deeply clone Map', function () { From f07aba22af28886d83edeba9f3fa6f20b9ca5abd Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Wed, 3 Aug 2022 13:57:43 +0200 Subject: [PATCH 11/12] perf: a little performance gain; calc is array|object once & circ on objs only --- index.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index f3271fe..a69add0 100644 --- a/index.js +++ b/index.js @@ -43,7 +43,7 @@ function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { return instanceClone(val); } - if (instanceClone || Array.isArray(val) || isPlainObject(val)) { + if (instanceClone || isArrayOrPlainObject(val)) { let res; if (root) { res = root; // don't directly use root to avoid confusion @@ -54,11 +54,14 @@ function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { // if root is undefined then this was just a clone object constructor if (root) { for (let key in val) { - let circularIndex = findIndex(parentsVal, v => isEqual(v, val[key])); - if (~circularIndex) { + const isValObj = isArrayOrPlainObject(val[key]); + let circularIndex; + if (isValObj) circularIndex = findIndex(parentsVal, v => isEqual(v, val[key])); + + if (circularIndex !== undefined && ~circularIndex) { res[key] = parentsRes[circularIndex]; } else { - if (Array.isArray(val[key]) || isPlainObject(val[key]) || typeof instanceClone === typeof val[key]) { + if (isValObj || typeof instanceClone === typeof val[key]) { // this is some kind of object parentsVal.push(val[key]); @@ -83,6 +86,11 @@ function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { } +function isArrayOrPlainObject (val) { + return Array.isArray(val) || isPlainObject(val); +} + + /** * Expose `cloneDeep` */ From f80668dde8b68d21a1c92db66c32079cc6f8df51 Mon Sep 17 00:00:00 2001 From: Emmanuel Mahuni Date: Wed, 3 Aug 2022 16:25:37 +0200 Subject: [PATCH 12/12] fix: instance true cloning --- index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index a69add0..cfb13fc 100644 --- a/index.js +++ b/index.js @@ -54,14 +54,15 @@ function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { // if root is undefined then this was just a clone object constructor if (root) { for (let key in val) { - const isValObj = isArrayOrPlainObject(val[key]); + const isValObj = isArrayOrObject(val[key]); + let circularIndex; if (isValObj) circularIndex = findIndex(parentsVal, v => isEqual(v, val[key])); if (circularIndex !== undefined && ~circularIndex) { res[key] = parentsRes[circularIndex]; } else { - if (isValObj || typeof instanceClone === typeof val[key]) { + if (isValObj || (instanceClone && (typeof instanceClone === typeof val[key] || typeof val[key] === 'function'))) { // this is some kind of object parentsVal.push(val[key]); @@ -91,6 +92,11 @@ function isArrayOrPlainObject (val) { } +function isArrayOrObject (val) { + return Array.isArray(val) || typeOf(val) === 'object'; +} + + /** * Expose `cloneDeep` */