Last active
September 2, 2018 00:12
-
-
Save prantlf/6d134475af24d4b2f7de to your computer and use it in GitHub Desktop.
JavaScript deepClone + integration to Undescore.js and Backbone.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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); | |
| })); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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