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 diff --git a/index.js b/index.js index 9133c08..cfb13fc 100644 --- a/index.js +++ b/index.js @@ -7,41 +7,96 @@ 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) { + +function cloneDeep (val, instanceClone) { + // 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) || typeof val === 'object') { + return _cloneDeep(val, instanceClone, parentsRes[0], parentsRes, [val]); + } + } + return parentsRes[0]; +} + + +function _cloneDeep (val, instanceClone, root, parentsRes, parentsVal) { switch (typeOf(val)) { - case 'object': - return cloneObjectDeep(val, instanceClone); case 'array': - return cloneArrayDeep(val, instanceClone); + case 'object': + return cloneObjectDeep(val, instanceClone, root, parentsRes, parentsVal); default: { return clone(val); } } } -function cloneObjectDeep(val, instanceClone) { + +function cloneObjectDeep (val, instanceClone, root, parentsRes, parentsVal) { if (typeof instanceClone === 'function') { return instanceClone(val); } - if (instanceClone || isPlainObject(val)) { - const res = new val.constructor(); - for (let key in val) { - res[key] = cloneDeep(val[key], instanceClone); + + if (instanceClone || isArrayOrPlainObject(val)) { + 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 root is undefined then this was just a clone object constructor + if (root) { + for (let key in val) { + 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 || (instanceClone && (typeof instanceClone === typeof val[key] || typeof val[key] === 'function'))) { + // 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); + } + } + } + } + return res; } + return val; } -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); - } - return res; + +function isArrayOrPlainObject (val) { + return Array.isArray(val) || isPlainObject(val); +} + + +function isArrayOrObject (val) { + return Array.isArray(val) || typeOf(val) === 'object'; } + /** * Expose `cloneDeep` */ diff --git a/package.json b/package.json index 7b494ea..b98eb03 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", + "lodash.findindex": "^4.6.0", + "lodash.isequal": "^4.5.0", "shallow-clone": "^3.0.0" }, "devDependencies": { diff --git a/test.js b/test.js index a93725e..b301f92 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,64 +28,101 @@ 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 Map', 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); + two.b.cyclic.c = 'e'; + assert.notDeepEqual(one, two); + assert.equal(two.b, two.b.cyclic); + assert.notEqual(one.b.cyclic.c, two.b.cyclic.c); + }); + + 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); + 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('should deeply clone arrays with circular references', function () { + const one = { a: 'b' }; + const arr1 = [one]; + arr1.push(arr1); + const arr2 = clone(arr1); + one.a = 'c'; + assert.notDeepEqual(arr1, arr2); + assert.equal(arr2, arr2[1]); + assert.notEqual(arr1[1][0].a, arr2[1][0].a); + }); + + 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); @@ -97,18 +134,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); @@ -120,19 +159,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) {