Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle circular references #24

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -19,6 +19,7 @@ const cloneDeep = require('clone-deep');

let obj = { a: 'b' };
let arr = [obj];

let copy = cloneDeep(arr);
obj.c = 'd';

Expand All @@ -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!
Expand Down Expand 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

Expand Down
85 changes: 70 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
*/
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
101 changes: 71 additions & 30 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand All @@ -15,77 +15,114 @@ 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]);
assert(fixture[1] !== result[1]);
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);
Expand All @@ -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);
Expand All @@ -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) {
Expand Down