Skip to content

Instantly share code, notes, and snippets.

@ElliotChong
Last active November 28, 2020 23:55
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save ElliotChong/3861963 to your computer and use it in GitHub Desktop.
Save ElliotChong/3861963 to your computer and use it in GitHub Desktop.
Copy all of the properties in the source objects over to the destination object, and return the destination object. This method will recursively copy mutual properties which are also objects.
# Create a deep copy of an object. - CoffeeScript conversion of @cederberg's deepClone implementation https://github.com/documentcloud/underscore/pull/595
deepClone = (obj) ->
if !_.isObject(obj) or _.isFunction(obj) then return obj
if _.isDate obj then return new Date do obj.getTime
if _.isRegExp obj then return new RegExp obj.source, obj.toString().replace(/.*\//, "")
isArr = _.isArray obj or _.isArguments obj
func = (memo, value, key) ->
if isArr then memo.push deepClone value
else memo[key] = deepClone value
return memo;
return _.reduce obj, func, if isArr then [] else {}
# Is a given value a basic Object? i.e.: {} || new Object()
isBasicObject = (object) ->
(object.prototype is {}.prototype or object.prototype is Object.prototype) and _.isObject(object) and not _.isArray(object) and not _.isFunction(object) and not _.isDate(object) and not _.isRegExp(object) and not _.isArguments(object)
# Returns a list of the names of every object in an object — that is to say, the name of every property of the object that is an object.
basicObjects = (object) ->
_.filter _.keys(object), (key) -> isBasicObject object[key]
# Returns a list of the names of every array in an object — that is to say, the name of every property of the object that is an array.
arrays = (object) ->
_.filter(_.keys(object), (key) -> _.isArray object[key])
# Copy and combine all of the properties in the source objects over to the destination object and return the destination object. This method will recursively copy shared properties which are also objects and combine arrays.
deepExtendCouple = (destination, source, maxDepth=20) ->
if maxDepth <= 0
console.warn '_.deepExtend(): Maximum depth of recursion hit.'
return _.extend destination, source
sharedObjectKeys = _.intersection(basicObjects(destination), basicObjects(source))
recurse = (key) ->
source[key] = deepExtendCouple destination[key], source[key], maxDepth-1
recurse sharedObjectKey for sharedObjectKey in sharedObjectKeys
sharedArrayKeys = _.intersection(arrays(destination), arrays(source))
combine = (key) ->
source[key] = _.union destination[key], source[key]
combine sharedArrayKey for sharedArrayKey in sharedArrayKeys
_.extend destination, source
# Copy and combine all of the properties in the supplied objects from right to left and return the combined object. This method will recursively copy shared properties which are also objects and combine arrays.
deepExtend = (objects..., maxDepth) ->
if !_.isNumber maxDepth
objects.push maxDepth
maxDepth = 20
if objects.length <= 1 then return objects[0]
if maxDepth <= 0 then return _.extend.apply this, objects
finalObj = do objects.shift
while objects.length > 0
finalObj = deepExtendCouple(finalObj, deepClone(do objects.shift), maxDepth)
return finalObj
_.mixin
deepClone: deepClone
isBasicObject: isBasicObject
basicObjects: basicObjects
arrays: arrays
deepExtend: deepExtend
foo =
array: [3, 4, 5, 1, 'rawr', 'foo']
name:
first: "Earl"
last: "Jones"
job: "Winning"
friends:
"01234":
name:
first: "Whodat"
bar =
array: [1, 2, 3, 'rawr', 'zoom']
name:
middle: "Samus"
job: "Farmer"
friends:
"01234":
name:
last: "Mook"
friends:
deep:
connection:
going:
all:
the:
combined: "Heyo!"
way:
down:
depth: "Too Deep?"
hear: "Not Too Deep"
blam =
name:
first: "Eddie"
job: "Mugger"
friends:
"01234":
name:
first: "Honorary"
friends:
deep:
connection:
going:
all:
the:
way:
down:
depth: "Way Too Deep!"
see: "What I mean?"
"43210":
name:
first: "Guy"
last: "Rogers"
# Use deepClone when testing because deepExtend will modify the first parameter
console.log(_.deepClone(foo),_.deepClone(bar),_.deepClone(blam))
console.log('default', _.deepExtend(_.deepClone(foo), bar, blam))
console.log('0', _.deepExtend(_.deepClone(foo), bar, blam, 0))
console.log('20', _.deepExtend(_.deepClone(foo), bar, blam, 20))
console.log('mixed-default', _.deepExtend(_.deepClone(blam), foo, bar, 20))
console.log('mixed-0', _.deepExtend(_.deepClone(blam), foo, bar, 0))
console.log('mixed-20', _.deepExtend(_.deepClone(blam), foo, bar, 20))
console.log(foo,bar,blam)
(function() {
var arrays, basicObjects, deepClone, deepExtend, deepExtendCouple, isBasicObject,
__slice = [].slice;
deepClone = function(obj) {
var func, isArr;
if (!_.isObject(obj) || _.isFunction(obj)) {
return obj;
}
if (_.isDate(obj)) {
return new Date(obj.getTime());
}
if (_.isRegExp(obj)) {
return new RegExp(obj.source, obj.toString().replace(/.*\//, ""));
}
isArr = _.isArray(obj || _.isArguments(obj));
func = function(memo, value, key) {
if (isArr) {
memo.push(deepClone(value));
} else {
memo[key] = deepClone(value);
}
return memo;
};
return _.reduce(obj, func, isArr ? [] : {});
};
isBasicObject = function(object) {
return (object.prototype === {}.prototype || object.prototype === Object.prototype) && _.isObject(object) && !_.isArray(object) && !_.isFunction(object) && !_.isDate(object) && !_.isRegExp(object) && !_.isArguments(object);
};
basicObjects = function(object) {
return _.filter(_.keys(object), function(key) {
return isBasicObject(object[key]);
});
};
arrays = function(object) {
return _.filter(_.keys(object), function(key) {
return _.isArray(object[key]);
});
};
deepExtendCouple = function(destination, source, maxDepth) {
var combine, recurse, sharedArrayKey, sharedArrayKeys, sharedObjectKey, sharedObjectKeys, _i, _j, _len, _len1;
if (maxDepth == null) {
maxDepth = 20;
}
if (maxDepth <= 0) {
console.warn('_.deepExtend(): Maximum depth of recursion hit.');
return _.extend(destination, source);
}
sharedObjectKeys = _.intersection(basicObjects(destination), basicObjects(source));
recurse = function(key) {
return source[key] = deepExtendCouple(destination[key], source[key], maxDepth - 1);
};
for (_i = 0, _len = sharedObjectKeys.length; _i < _len; _i++) {
sharedObjectKey = sharedObjectKeys[_i];
recurse(sharedObjectKey);
}
sharedArrayKeys = _.intersection(arrays(destination), arrays(source));
combine = function(key) {
return source[key] = _.union(destination[key], source[key]);
};
for (_j = 0, _len1 = sharedArrayKeys.length; _j < _len1; _j++) {
sharedArrayKey = sharedArrayKeys[_j];
combine(sharedArrayKey);
}
return _.extend(destination, source);
};
deepExtend = function() {
var finalObj, maxDepth, objects, _i;
objects = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), maxDepth = arguments[_i++];
if (!_.isNumber(maxDepth)) {
objects.push(maxDepth);
maxDepth = 20;
}
if (objects.length <= 1) {
return objects[0];
}
if (maxDepth <= 0) {
return _.extend.apply(this, objects);
}
finalObj = objects.shift();
while (objects.length > 0) {
finalObj = deepExtendCouple(finalObj, deepClone(objects.shift()), maxDepth);
}
return finalObj;
};
_.mixin({
deepClone: deepClone,
isBasicObject: isBasicObject,
basicObjects: basicObjects,
arrays: arrays,
deepExtend: deepExtend
});
}).call(this);
var user = { "name": { "first": "Earl" }, "friends": ["0","1","3"], "job": "Mad Scientist", "pets": { "dog": "Ralph" } };
var userUpdate = { "name": { "last": "Duncan" }, "friends": ["6","9"], "job": "Happy Scientist", "pets": { "cat": "Judy" }, "city": "Portlandia" };
_.deepExtend(user, userUpdate);
// results in user equalling:
// { "name": { "first": "Earl", "last": "Duncan" }, "friends": ["0","1","3","6","9"], "job": "Happy Scientist", "pets": { "dog": "Ralph", "cat": "Judy" }, "city": "Portlandia" }
var user = { "name": { "first": "Earl" }, "friends": ["0","1","3"], "job": "Mad Scientist", "pets": { "dog": "Ralph" } };
var userUpdate = { "name": { "last": "Duncan" }, "friends": ["6","9"], "job": "Happy Scientist", "pets": { "cat": "Judy" }, "city": "Portlandia" };
_.extend(user, userUpdate);
// results in user equalling:
// { "name": { "last": "Duncan" }, "friends": ["6","9"], "job": "Happy Scientist", "pets": { "cat": "Judy" }, "city": "Portlandia" }
@senica
Copy link

senica commented Feb 25, 2013

As dschi mentioned for the coffescript version, the js version should be
if (!_.isObject(obj) || _.isFunction(obj)) {
instead of
if (!_.isObject(obj || _.isFunction(obj))) {

@AndrewEastwood
Copy link

What about this:

var clone = JSON.parse(JSON.stringify(obj));

@ElliotChong
Copy link
Author

Great catch @dschi and @senica, thanks!

@ElliotChong
Copy link
Author

@AndrewEastwood Nice deepClone replacement! You'll lose Date and RegEx handling, but if you're not using those data types that should work well.

@ElliotChong
Copy link
Author

@chrisritter Thanks for asking! License is WTFPL.
http://en.wikipedia.org/wiki/WTFPL

@JarnoLeConte
Copy link

'null' values don't work. I get an error in isBasicObject:

TypeError: Cannot read property 'prototype' of null

The problem can be solved by switching the order of the first two clauses in the logical expression in isBasicObject().

  _.isObject(object) && (object.prototype === {}.prototype || object.prototype === Object.prototype)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment