Skip to content

Instantly share code, notes, and snippets.

@prantlf
Last active September 2, 2018 00:12
Show Gist options
  • Select an option

  • Save prantlf/6d134475af24d4b2f7de to your computer and use it in GitHub Desktop.

Select an option

Save prantlf/6d134475af24d4b2f7de to your computer and use it in GitHub Desktop.
JavaScript deepClone + integration to Undescore.js and Backbone.js
// Performs a deep clone of primitive values, arrays, object literals,
// and arbitrary objects, if requested. It is supposed to clone object
// literals, which are going to be modified, but the original should
// stay intact.
//
// The default behaviour works well with JSON REST API responses and
// and other objects, which contain only values of native types.
// If functions or HTML elements are encountered, they will not be
// actually cloned; they will be just copied, which works well with
// the typical options objects for Backbone constructors.
// If arbitrary objects can be encountered, additional options can
// be used to specify the result:
//
// default behaviour
// : Throws an error
// cloneCloneableObjects: true
// : Uses deepClone or clone method, if available.
// cloneArbitraryObjects: true
// : Creates new objects with clones own properties of the original.
// copyArbitraryObjects
// : Copies the original.
//
// This function depends on Underscore.js.
function deepClone(source, options) {
var result = {},
key;
if (!source || typeof source !== 'object' || source instanceof HTMLElement ||
_.isBoolean(source) || _.isNumber(source) || _.isString(source)) {
return source;
}
if (_.isDate(source) || _.isRegExp(source)) {
return new source.constructor(source.valueOf());
}
if (_.isArray(source)) {
return _.map(source, function (obj) {
return _.deepClone(obj);
});
}
if (source.constructor !== Object) {
if (options) {
if (options.cloneCloneableObjects) {
if (source.deepClone) {
return source.deepClone();
}
if (source.clone) {
return source.clone();
}
}
if (options.copyArbitraryObjects) {
return source;
}
if (!options.cloneArbitraryObjects) {
throw new Error('Cannot clone an arbitrary function object instance');
}
} else {
throw new Error('Cannot clone an arbitrary function object instance');
}
}
return _.reduce(source, function (result, value, key) {
result[key] = deepClone(value);
return result;
}, {});
}
// Expose deepClone among the Underscore.js methods
_.mixin({deepClone: deepClone});
// Add deepClone to the Backbone.Model prototype
Backbone.Model.prototype.deepClone = function (options) {
return new this.constructor(deepClone(this.attributes, options));
};
// Add deepClone to the Backbone.Collection prototype
Backbone.Collection.prototype.deepClone = function (options) {
return new this.constructor(this.map(function (model) {
return deepClone(model.attributes, options);
}));
};
describe('deepClone', function () {
it('copies immutable literals', function () {
expect(undefined).toBe(deepClone(undefined));
expect(null).toBe(deepClone(null));
expect(false).toBe(deepClone(false));
expect(true).toBe(deepClone(true));
expect(0).toBe(deepClone(0));
expect(1).toBe(deepClone(1));
expect(1.2).toBe(deepClone(1.2));
expect(Infinity).toBe(deepClone(Infinity));
expect('').toBe(deepClone(''));
expect('s').toBe(deepClone('s'));
});
it('copies immutable built-ins', function () {
var original = new Boolean(true), // jshint ignore:line
clone = deepClone(original);
expect(original.valueOf()).toEqual(clone.valueOf());
original = new Number(1); // jshint ignore:line
clone = deepClone(original);
expect(original.valueOf()).toEqual(clone.valueOf());
original = String('s');
clone = deepClone(original);
expect(original.valueOf()).toEqual(clone.valueOf());
});
it('clones mutable built-ins', function () {
var original = new Date(),
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original.valueOf()).toEqual(clone.valueOf());
original = /r/;
clone = deepClone(original);
expect(original.valueOf()).toEqual(clone.valueOf());
});
it('clones objects', function () {
var original = {},
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original.valueOf()).toEqual(clone.valueOf());
original = {k: 1};
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original.valueOf()).toEqual(clone.valueOf());
original = {k: {k: 1}};
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original.k).not.toBe(clone.k);
expect(original.valueOf()).toEqual(clone.valueOf());
});
it('clones arrays', function () {
var original = [],
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original.valueOf()).toEqual(clone.valueOf());
original = [1];
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original.valueOf()).toEqual(clone.valueOf());
original = [[1]];
clone = deepClone(original);
expect(original).not.toBe(clone);
expect(original[0]).not.toBe(clone[1]);
expect(original.valueOf()).toEqual(clone.valueOf());
});
it('copies functions and HTML elements', function () {
var original = function () {},
clone = deepClone(original);
expect(original).toBe(clone);
original = document.body;
clone = deepClone(original);
expect(original).toBe(clone);
});
it('can copy or clone arbitrary objects', function () {
var original = $(document.body),
clone = deepClone(original, {copyArbitraryObjects: true});
expect(original).toBe(clone);
original = $(document.body);
clone = deepClone(original, {cloneArbitraryObjects: true});
expect(original).not.toBe(clone);
expect(original[0].tagName).toEqual(clone[0].tagName);
original = $(document.body);
clone = deepClone(original, {cloneCloneableObjects: true});
expect(original).not.toBe(clone);
expect(original[0].tagName).toEqual(clone[0].tagName);
original = $(document.body);
expect(function () {
deepClone(original);
}).toThrow(new Error('Cannot clone an arbitrary function object instance'));
});
it('integrates to Underscore.js and Backbone.js', function () {
expect(_.deepClone).toBe(deepClone);
expect(Backbone.Model.prototype.deepClone).toBeDefined();
expect(Backbone.Collection.prototype.deepClone).toBeDefined();
});
it('clones Backbone models and collections', function () {
var original = new Backbone.Model({k: 1}),
clone = original.deepClone();
expect(original).not.toBe(clone);
expect(original.attributes).toEqual(clone.attributes);
original = new Backbone.Collection([{k: 1}]);
clone = original.deepClone();
expect(original).not.toBe(clone);
expect(original.length).toEqual(clone.length);
expect(original.first()).not.toBe(clone.first());
expect(original.first().attributes).toEqual(clone.first().attributes);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment