diff --git a/src/.jshintrc b/src/.jshintrc index 55414b5b3fb8..fdb544d3fc9b 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -34,6 +34,7 @@ "nextUid": false, "setHashKey": false, "extend": false, + "merge": false, "int": false, "inherit": false, "noop": false, diff --git a/src/Angular.js b/src/Angular.js index 80a9eadb9795..5d217c8f25d2 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -323,6 +323,30 @@ function setHashKey(obj, h) { } } + +function baseExtend(dst, objs, deep) { + for (var i = 0, ii = objs.length; i < ii; ++i) { + var obj = objs[i]; + if (!isObject(obj) && !isFunction(obj)) continue; + var keys = Object.keys(obj); + for (var j = 0, jj = keys.length; j < jj; j++) { + var key = keys[j]; + if (key === '$$hashKey') continue; + var src = obj[key]; + + if (deep && isObject(src)) { + if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; + baseExtend(dst[key], [src], true); + } else { + dst[key] = src; + } + } + } + + return dst; +} + + /** * @ngdoc function * @name angular.extend @@ -333,28 +357,36 @@ function setHashKey(obj, h) { * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`. - * Note: Keep in mind that `angular.extend` does not support recursive merge (deep copy). * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. */ function extend(dst) { - var h = dst.$$hashKey; + return baseExtend(dst, slice.call(arguments, 1), false); +} - for (var i = 1, ii = arguments.length; i < ii; i++) { - var obj = arguments[i]; - if (obj) { - var keys = Object.keys(obj); - for (var j = 0, jj = keys.length; j < jj; j++) { - var key = keys[j]; - dst[key] = obj[key]; - } - } - } - setHashKey(dst, h); - return dst; +/** +* @ngdoc function +* @name angular.merge +* @module ng +* @kind function +* +* @description +* Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s) +* to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so +* by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`. +* +* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source +* objects, performing a deep copy. +* +* @param {Object} dst Destination object. +* @param {...Object} src Source object(s). +* @returns {Object} Reference to `dst`. +*/ +function merge(dst) { + return baseExtend(dst, slice.call(arguments, 1), true); } function int(str) { diff --git a/src/AngularPublic.js b/src/AngularPublic.js index b81257b9fff7..db9e2be0141c 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -116,6 +116,7 @@ function publishExternalAPI(angular) { 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, + 'merge': merge, 'equals': equals, 'element': jqLite, 'forEach': forEach, diff --git a/test/.jshintrc b/test/.jshintrc index 6bca8382f2d9..87d775ae7f03 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -31,6 +31,7 @@ "nextUid": false, "setHashKey": false, "extend": false, + "merge": false, "int": false, "inherit": false, "noop": false, diff --git a/test/AngularSpec.js b/test/AngularSpec.js index e63789100947..3800b4c01c30 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -225,6 +225,74 @@ describe('angular', function() { }); }); + + describe('merge', function() { + it('should recursively copy objects into dst from left to right', function() { + var dst = { foo: { bar: 'foobar' }}; + var src1 = { foo: { bazz: 'foobazz' }}; + var src2 = { foo: { bozz: 'foobozz' }}; + merge(dst, src1, src2); + expect(dst).toEqual({ + foo: { + bar: 'foobar', + bazz: 'foobazz', + bozz: 'foobozz' + } + }); + }); + + + it('should replace primitives with objects', function() { + var dst = { foo: "bloop" }; + var src = { foo: { bar: { baz: "bloop" }}}; + merge(dst, src); + expect(dst).toEqual({ + foo: { + bar: { + baz: "bloop" + } + } + }); + }); + + + it('should replace null values in destination with objects', function() { + var dst = { foo: null }; + var src = { foo: { bar: { baz: "bloop" }}}; + merge(dst, src, true); + expect(dst).toEqual({ + foo: { + bar: { + baz: "bloop" + } + } + }); + }); + + + it('should copy references to functions by value rather than merging', function() { + function fn() {} + var dst = { foo: 1 }; + var src = { foo: fn }; + merge(dst, src); + expect(dst).toEqual({ + foo: fn + }); + }); + + + it('should create a new array if destination property is a non-object and source property is an array', function() { + var dst = { foo: NaN }; + var src = { foo: [1,2,3] }; + merge(dst, src, true); + expect(dst).toEqual({ + foo: [1,2,3] + }); + expect(dst.foo).not.toBe(src.foo); + }); + }); + + describe('shallow copy', function() { it('should make a copy', function() { var original = {key:{}};