Skip to content

Instantly share code, notes, and snippets.

@jherax
Last active May 30, 2020 15:42
Show Gist options
  • Save jherax/05204bdf9eb47eeffdc8 to your computer and use it in GitHub Desktop.
Save jherax/05204bdf9eb47eeffdc8 to your computer and use it in GitHub Desktop.
Clones or extends an object (deep copy) supporting objects with circular references
/**
* @author
* David Rivera (jherax)
* https://github.com/jherax
*/
/* eslint-disable no-bitwise */
/** @private */
const toString = Object.prototype.toString;
const constructors = [Date, RegExp, Function, String, Number, Boolean];
const isNotObject = value => value === null || typeof value !== 'object';
// Add support for some ES2015 constructors
['Map', 'Set'].forEach((c) => {
if (c in window) constructors.push(window[c]);
});
/**
* @private
* Compares an object against the context.
*
* @param {any} obj: the object to compare
* @returns {Boolean}
*/
function compare(obj) {
return obj === this;
}
/**
* @private
* Creates a deep copy of an object supporting circular structures.
*
* @param {any} source: the object to clone
* @param {Object} dest: (optional) object to extend
* @param {Array} cache: the cache for cloned objects
* @return {any} the object cloned
*/
function cloner(source, dest, cache) {
let prop;
// determines whether @source is a primitive value or a function
if (isNotObject(source)) return source;
// checks if @source refers to an object created previously
if (toString.call(source) === '[object Object]') {
if (cache.some(compare, source)) return source;
// keeps references of created objects
// to prevent a circular structure exception
cache.push(source);
}
// determines if the @source is a DOM Node
if (source.nodeName) return source.cloneNode(true);
// determines whether @source is an instance of any of the constructors
if (~constructors.indexOf(source.constructor)) return new source.constructor(source);
if (source.constructor !== Object && source.constructor !== Array) return source;
// creates a new object and recursively iterates over its properties
dest = dest || new source.constructor();
for (prop in source) {
// merges @source into @dest
dest[prop] = cloner(source[prop], dest[prop], cache);
}
return dest;
}
/**
* @public
* Creates a deep copy of an object supporting objects with circular references.
*
* @param {any} source: the object to clone
* @return {any}
*/
function clone(source) {
// the cache prevents circular reference exception!
return cloner(source, null, []);
}
/**
* @public
* Merges an object into another supporting objects with circular references.
* Recursively descends into object properties of source objects, performing a deep copy.
*
* @param {Object} dest: destination object to merge
* @param {Object} source: the object to copy from
* @return {Object}
*/
function merge(dest, source) {
if (dest === source) return dest;
if (isNotObject(dest)) throw Error('First argument must be an object');
cloner(source, dest, []);
return dest;
}
var freeman, david, cloned;
function Freeman() {
"use strict";
this.name = "Gordon Freeman";
this.character = "Freeman";
this.friends = ["Barney Calhoun"];
}
david = {
name: "David",
character: "jherax",
friends: ["John Carmack"],
languages: /javascript|c#|java|vb|sql/i,
greeting: function() { return "Hi, I am " + this.name },
node: document.getElementsByClassName('js-code-editor')[1],
info: { birth: new Date() }
};
// let's create a circular reference
freeman = new Freeman();
freeman.friends.push(david);
david.friends.push(freeman);
// let's clone an object with circular reference
// No circular reference exception!!
var cloned = clone(david);
// let's modify some properties
cloned.name = "David Rivera";
cloned.friends.push("Jim Rynor");
// the original object was not affected!
console.log("original:", david.name, david.friends);
console.log("cloned:", cloned.name, cloned.friends);
/**
* Merging objects...
*/
var config = {
container: 'body',
position: {
left: 0,
top: 0,
},
};
var options = {
element: '.tooltip',
position: {left: 'center'},
};
// merge the "options" object into "config"
// note the "position" property is not overridden but merged
// (Object.assign overrides existing properties)
merge(config, options);
console.log(config);
/**
* Merging arrays...
*/
var dest = [1, 2, {a: 'a'}];
merge(dest, [3, null, {b: 'b'}]);
console.log(dest);
// [3, null, {a: "a", b: "b"}]
@jherax
Copy link
Author

jherax commented May 26, 2015

From the article: Clonando objetos en JavaScript
Follow the blog: jherax.wordpress.com

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