diff --git a/src/core/util.ts b/src/core/util.ts index 9100c7f3..264210de 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -75,10 +75,13 @@ export function logError(...args: any[]) { * (There might be a large number of date in `series.data`). * So date should not be modified in and out of echarts. */ -export function clone(source: T): T { +function cloneHelper(source: T, alreadyCloned: Map): T { if (source == null || typeof source !== 'object') { return source; } + if (alreadyCloned.has(source)) { + return alreadyCloned.get(source) as T; + } let result = source as any; const typeStr = objToString.call(source); @@ -86,8 +89,9 @@ export function clone(source: T): T { if (typeStr === '[object Array]') { if (!isPrimitive(source)) { result = [] as any; + alreadyCloned.set(source, []); for (let i = 0, len = (source as any[]).length; i < len; i++) { - result[i] = clone((source as any[])[i]); + result[i] = cloneHelper((source as any[])[i], alreadyCloned); } } } @@ -108,17 +112,24 @@ export function clone(source: T): T { } else if (!BUILTIN_OBJECT[typeStr] && !isPrimitive(source) && !isDom(source)) { result = {} as any; + alreadyCloned.set(source, result); for (let key in source) { // Check if key is __proto__ to avoid prototype pollution if (source.hasOwnProperty(key) && key !== protoKey) { - result[key] = clone(source[key]); + result[key] = cloneHelper(source[key], alreadyCloned); } } } + alreadyCloned.delete(source); return result; } +export function clone(source: T): T { + const alreadyCloned = new Map(); + return cloneHelper(source, alreadyCloned); +} + export function merge< T extends Dictionary, S extends Dictionary diff --git a/test/ut/spec/core/util.test.ts b/test/ut/spec/core/util.test.ts index 61847185..6e25b8d8 100755 --- a/test/ut/spec/core/util.test.ts +++ b/test/ut/spec/core/util.test.ts @@ -146,5 +146,33 @@ describe('zrUtil', function() { }); + it("circular", function () { + class TreeNode { + public children: TreeNode[] + public parent: TreeNode | null + + constructor(parent: TreeNode = null, children: TreeNode[] = []) { + this.children = children; + this.parent = parent; + } + } + + const root = new TreeNode(); + const a = new TreeNode(root, [new TreeNode(), new TreeNode()]); + a.children.forEach(c => c.parent = a); + root.children.push(a) + root.children.push(new TreeNode(root, [new TreeNode()])) + expect(zrUtil.clone(root)).toEqual(root); + + const b: { key: any } = {key: null}; + b.key = b; + const bCloned = zrUtil.clone(b); + expect(bCloned === b).toBeFalsy(); + expect(bCloned.key === b).toBeFalsy(); + expect(bCloned === b.key).toBeFalsy(); + expect(bCloned.key === b.key).toBeFalsy(); + expect(bCloned).toEqual(b); + }); + }); }); \ No newline at end of file