diff --git a/test/objects.js b/test/objects.js index 1abdce21f..80e6a1ce2 100644 --- a/test/objects.js +++ b/test/objects.js @@ -286,6 +286,41 @@ assert.strictEqual(_.clone(null), null, 'non objects should not be changed by clone'); }); + QUnit.test('cloneDeep', function(assert) { + var source = { + str: 'str', + num: 1, + bool: false, + list: [1, 2, 3], + obj: {a: 'a', b: 'b'}, + nu: null, + und: void 0 + }; + var clone = _.cloneDeep(source); + assert.strictEqual(clone.str, 'str', 'can clone string value from source'); + assert.strictEqual(clone.num, 1, 'can clone number value from source'); + assert.strictEqual(clone.bool, false, 'can clone boolean value from source'); + assert.deepEqual(clone.list, [1, 2, 3], 'can clone array from source'); + assert.deepEqual(clone.obj, {a: 'a', b: 'b'}, 'can clone object from source'); + assert.deepEqual(clone.nu, null, 'can clone null value from source'); + assert.deepEqual(clone.und, void 0, 'can clone undefined value from source'); + + assert.notStrictEqual(clone.list, source.list, 'refers to a copy of the original array'); + assert.notStrictEqual(clone.obj, source.obj, 'refers to a copy of the original object'); + + function F() {} + F.prototype = {a: 'a'}; + var subObj = new F(); + subObj.b = 'b'; + clone = _.cloneDeep(subObj); + assert.deepEqual(clone, {a: 'a', b: 'b'}, 'copies all properties from source'); + + source = {a: 'a'}; + source.obj = source; + clone = _.cloneDeep(source); + assert.deepEqual(clone.obj.a, 'a', 'can deal with circular references cases'); + }); + QUnit.test('create', function(assert) { var Parent = function() {}; Parent.prototype = {foo: function() {}, bar: 2}; diff --git a/underscore.js b/underscore.js index ca1cdd332..8c9dfb3c1 100644 --- a/underscore.js +++ b/underscore.js @@ -1158,6 +1158,37 @@ return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; + // Create a (deep-cloned) duplicate of an object. + var cloneDeep = function(obj, stack) { + if (!_.isObject(obj)) { + return obj; + } + var keys = !isArrayLike(obj) && _.allKeys(obj), + length = (keys || obj).length, + result = keys ? {} : []; + + if (!stack) { + stack = [[], []]; + } + var stacked = _.indexOf(stack[0], obj); + if (stacked > -1) { + return stack[1][stacked]; + } + stack[0].push(obj); + stack[1].push(result); + + for (var i = 0; i < length; i++) { + var key = keys ? keys[i] : i; + result[key] = cloneDeep(obj[key], stack); + } + + return result; + }; + + _.cloneDeep = function(obj) { + return cloneDeep(obj); + }; + // Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain.