Skip to content

Handle circular references #24

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

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.

@@ -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

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