DEV Community

loading...

Copying and extending Objects in javaScript

Leo Lanese
I'm a passionately curious Front-end Engineer. I like sharing what I know, and learning what I don't. London, UK. @leolaneseltd
・8 min read

1) Copying Objects
--[1.1] Copying plain Objects
--[1.2] Copying deeply nested Objects

2) Extending Objects
--[2.1] Extending plain Objects
--[2.2] Extending deeply nested Objects


1) Copying plain Objects:

[1.1] Copying plain Objects

Simple Array of Object

const object = {
  'color': 'red'
};

// shallow copy
copyObjectAssign = Object.assign({}, object);
// shallow copy
copySpread = { ...object};
// ~deep copy
copyJSONparse = JSON.parse(JSON.stringify(object));

object.color = 'blue'; // changing original object

object === copyJSONparse; // FALSE
object === copyObjectAssign; // FALSE
object === copySpread ; // FALSE

Enter fullscreen mode Exit fullscreen mode
// hidden setup JavaScript code goes in this preamble area const object = { 'color': 'red' }; // shallow copy copyObjectAssign = Object.assign({}, object); // shallow copy copySpread = { ...object}; // ~deep copy copyJSONparse = JSON.parse(JSON.stringify(object)); object.color = 'blue'; // changing original object console.log(object === copyJSONparse); // FALSE console.log(object === copyObjectAssign); // FALSE console.log(object === copySpread); // FALSE

Alt Text

[1.2] Copying deeply nested Objects

These are Objects that have more than one level deep

const objectNested = {
  "color": "red",
  "car": {
    "model": {
       year: 2020
    }
  }
};
// shallow copy
copyObjectAssignNested = Object.assign({}, objectNested );
// shallow copy
copySpreadNested = { ...objectNested };
// ~deep copy
copyJSONparseNested = JSON.parse(JSON.stringify(objectNested));

// changing the original objectNested
objectNested.car.model.year = 1975; // change here

// original object IS changed!
objectNested // {"color":"red", "car":{"model": { year: 1975 }}

// shallow-copy IS changed! 
copyObjectAssignNested // {"color":"red", "car":{"model": { year: 1975 }}
copySpreadNested // {"color":"red", "car": {"model": { year: 1975 }}

// deep-copy NOT changed: deepClone Object won't have any effect if the main source object obj is modified and vice-versa
copyJSONparseNested //  {"color":"red", "car": {"model": { year: 2020 }}

// let see what changes then?
JSON.stringify(objectNested) === JSON.stringify(copyObjectAssignNested); // TRUE
JSON.stringify(objectNested) === JSON.stringify(copySpreadNested); // TRUE
JSON.stringify(objectNested) === JSON.stringify(copyJSONparseNested);  // FALSE (changes don't affect each other after deep-copy)
Enter fullscreen mode Exit fullscreen mode
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 const objectNested = { "color": "red", "car": { "model": { year: 2020 } } }; // shallow copy copyObjectAssignNested = Object.assign({}, objectNested ); // shallow copy copySpreadNested = { ...objectNested }; // ~deep copy copyJSONparseNested = JSON.parse(JSON.stringify(objectNested)); // changing the original objectNested objectNested.car.model.year = 1975; // change here // original object IS changed! objectNested // {"color":"red", "car":{"model": { year: 1975 }} // shallow-copy IS changed! copyObjectAssignNested // {"color":"red", "car":{"model": { year: 1975 }} copySpreadNested // {"color":"red", "car": {"model": { year: 1975 }} // deep-copy NOT changed: deepClone Object won't have any effect if the main source object obj is modified and vice-versa copyJSONparseNested // {"color":"red", "car": {"model": { year: 2020 }} // let see what changes then? console.log(JSON.stringify(objectNested) === JSON.stringify(copyObjectAssignNested)); // TRUE console.log(JSON.stringify(objectNested) === JSON.stringify(copySpreadNested)); // TRUE console.log(JSON.stringify(objectNested) === JSON.stringify(copyJSONparseNested)); // FALSE (changes don't affect each other after deep-copy)

Alt Text
Alt Text

Why:

  • Object.assign({})
    Can only make shallow copies of objects so it will only work in a single level (first level) of the object reference.

  • Object spread:
    Object spread does a 'shallow copy' of the object. Only the object itself is cloned, while "nested instances are not cloned".

  • JSON.parse(JSON.stringify()):
    This is a questionable solution. Why? Because this is going to work fine as long as your Objects and the nested Objects "only contains primitives", but if you have objects containing functions or 'Date' this won't work.

Changing a property value from the original object or property value from the shallow copy object it will affect each other.

The reason is how the javascript engine works internally: JS passes the primitive values as value-copy and the compound values as reference-copy to the value of the primitives on that Object. So, when copied the Object containing the nested Object, that will create a shallow-copy of that Object:

Primitive found on the first level of the original object it will be copied as value-copy: changing is reciprocal: changing the value of one will affect the other one. So they will be depending on each other

Deeply nested properties will be copied as reference-copy: changing one it will not affect the reference of the other one

first-level properties: value-copy
deeply nested properties: reference-copy

Solution:

We can create our own or we can use the third-party libraries to achieve a future-proof deep copy and deep merge.

Third party solutions:

lodash's cloneDeep()

import * as cloneDeep from 'lodash/cloneDeep';
...
clonedObject = cloneDeep(originalObject);
Enter fullscreen mode Exit fullscreen mode
const objectNested = {
  "name":"John",
  "age":30,
  "cars": {
    "car1":"Ford",
    "car2":"BMW",
    "model": {
       year: 2020
    }
  }
};
 
// making a copy of the reference, a new object is created that has an exact copy of the values in the original object.
const deep = _.cloneDeep(objectNested);
console.log(JSON.stringify(deep) === JSON.stringify(objectNested)); // TRUE
console.log("deep reference", deep.cars.model === objectNested.cars.model); // FALSE

// assinging one Object to other reference
const deep2 = objectNested;
console.log('share reference', deep2.cars.model === objectNested.cars.model); // TRUE
console.log('share references', deep2 === objectNested); // TRUE
Enter fullscreen mode Exit fullscreen mode

Alt Text
Alt Text

Lodash cloneDeep()

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deepCopy = _.cloneDeep(objects);
console.log(deepCopy[0] === objects[0]); // => false
objects[0].a === deepCopy[0].a // true

deep[0].a = 123; // original object changes
objects[0].a === deepCopy[0].a // false = changes no affecting deepCopy
Enter fullscreen mode Exit fullscreen mode

Further Information:

Lodash
https://lodash.com/docs/4.17.15#cloneDeep

Lodash npm package:
https://www.npmjs.com/package/lodash.clonedeep

Immutability-helper:
A light and easy to use helper which allows us to mutate a copy of an object without changing the original source:
https://github.com/kolodny/immutability-helper


 2) Extending Objects

Few options we are going to evaluate:

   JS            |  JS ES6+          |    jQuey        |  Lodash     |  AngularJS
Object.assign()    Spread operator      $.extend()       .merge()      .extend()  
mix()                                                                  .merge() 
Enter fullscreen mode Exit fullscreen mode

 [2.1] Extending plain Objects

Extend Objects is a simple process but required to know what we want to do with:

  • Objects that have the same name attributes
  • Mutation of the Object

Object.assign({}):

let defaults = {
  container:       ".main",
  isActiveClass:   ".is-active"
};

let options1 = {
  container:       ".main-container",
  isActiveClass:   ".is-active-element"
};

let options2 = {
  aNewClass:       "somethingHere",
  isActiveClass:   ".is-active-content"
};

settings = Object.assign({}, defaults, options1, options2); // using {}
// { container: ".main-container", isActiveClass: ".is-active-content", aNewClass: "somethingHere"}
Enter fullscreen mode Exit fullscreen mode
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 // visible, reader-editable JavaScript code goes here let defaults = { container: ".main", isActiveClass: ".is-active" }; let options1 = { container: ".main-container", isActiveClass: ".is-active-element" }; let options2 = { aNewClass: "somethingHere", isActiveClass: ".is-active-content" }; console.log(settings = Object.assign({}, defaults, options1, options2)); // using {} // { container: ".main-container", isActiveClass: ".is-active-content", aNewClass: "somethingHere"}

Further information:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign


Custom .mix() method for ES5 and earlier:

Flat Object

Add an Object2 to another Object1:
$.extend() like, but DO NOT replace similar keys = the FIRST OBJECT WILL PREVAIL
We are navigating thought the flat object and '=' the values.

// source
options = {
  underscored: true,
  "name": 1
}
// target
products = {
  foo: false,
  "name": "leo"
}

function mix(source, target) {
  for(var key in source) {
     if (source.hasOwnProperty(key)) {
       target[key] = source[key];
     }
  }
 console.log(target)
}
mix(options, products); // { foo: false, name: 1, underscored: true }
Enter fullscreen mode Exit fullscreen mode
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 // visible, reader-editable JavaScript code goes here options = { underscored: true, "name": 1 } // target products = { foo: false, "name": "leo" } function mix(source, target) { for(var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } console.log(target) } console.log(mix(options, products)); // { foo: false, name: 1, underscored: true }

ES6 Spread operator

let defaults = {
  container:       "main",
  isActiveClass:   "is-active",
  code: {
    description: 'default code'
  }
};

let options1 = {
  container:       "main-container",
  isActiveClass:   "is-active-element",
  code: {
    description: 'options1 code'
  }
};

let options2 = {
  aNewClass:       "somethingHere",
  isActiveClass:   "is-active-content",
  code: {
    description: 'options2 code'
  }
};

mergedObj = { ...defaults , ...options1, ...options2 };

// { aNewClass: "somethingHere"
     code: {
        description: "options2 code"
     },
     container: "main-container"
     isActiveClass: "is-active-content"
   }
Enter fullscreen mode Exit fullscreen mode
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 // visible, reader-editable JavaScript code goes here let defaults = { container: "main", isActiveClass: "is-active", code: { description: 'default code' } }; let options1 = { container: "main-container", isActiveClass: "is-active-element", code: { description: 'options1 code' } }; let options2 = { aNewClass: "somethingHere", isActiveClass: "is-active-content", code: { description: 'options2 code' } }; console.log(mergedObj = { ...defaults , ...options1, ...options2 }); // { aNewClass: "somethingHere", code: {description: "options2 code"},container: "main-container",isActiveClass: "is-active-content"}

If some objects have a property with the same name, then the second object property overwrites the first. If we don't want this behaviour we need to perform a 'deep merge' or object and array recursive merge.

Alt Text

Further information:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax


$.extend()

It is a jQuery function that will extend and replace similar keys.

 $.extend() replace similar keys:

const defaults = { d1: false, d2: 5, d3: "foo" };
const options = { d4: true, d6: "bar" };

// jQuery Merge object2 into object1 (modifying there results)
$.extend( defaults, options );
// Object {d1: false, d2: 5, d3: "foo", d4: true, d6: "bar"}
Enter fullscreen mode Exit fullscreen mode

$.extend() without replace similar keys:

Remember that Javascript objects are mutable and store by reference.

const defaults = { validate: false, limit: 5, name: "foo" };
const options = { validate: true, name: "bar" };

// Merge defaults and options, without modifying defaults
settings = $.extend({}, defaults, options);
Object {validate: true, limit: 5, name: "bar"}
Enter fullscreen mode Exit fullscreen mode

Further information:
jquery.extend()
https://api.jquery.com/jquery.extend


Lodash .merge()

"This method is like _.assign except that it recursively merges own and inherited enumerable string keyed properties of source objects into the destination object. Source properties that resolve to undefined are skipped if a destination value exists. Array and plain object properties are merged recursively. Other objects and value types are overridden by assignment. Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources."
https://lodash.com/docs/4.17.15#merge

Note: This method mutates object.

Using Lodash .merge() with first level (flat) object

let defaults = {
  container:       "main",
  isActiveClass:   "is-active"
};

let options1 = {
  container:       "main-container",
  isActiveClass:   "is-active-element"
};

let options2 = {
  aNewClass:       "somethingHere",
  isActiveClass:   "is-active-content"
};

_.merge(defaults, options1, options2);

_.merge(defaults, options1, options2);
// { aNewClass: "somethingHere", container: "main-container", isActiveClass: "is-active-content"}
Enter fullscreen mode Exit fullscreen mode

Alt Text

Using Lodash .merge() with deeply nested object:

let defaults = {
  container:       "main",
  isActiveClass:   "is-active",
  code: {
    description: 'default code'
  }
};

let options1 = {
  container:       "main-container",
  isActiveClass:   "is-active-element",
  code: {
    description: 'options1 code'
  }
};

let options2 = {
  aNewClass:       "somethingHere",
  isActiveClass:   "is-active-content",
  code: {
    description: 'options2 code'
  }
};

_.merge(defaults, options1, options2);

// {
  aNewClass: "somethingHere"
  code: {
    description: "options2 code"
  },   
  container: "main-container"
  isActiveClass: "is-active-content"
}
Enter fullscreen mode Exit fullscreen mode

 [2.2] Extending deeply nested Objects

AngularJS 'angular.extend()' and 'angular.merge()':

angular.merge() it will be preserving properties in child objects.

angular.extend() it will not preserve, it will replace similar properties

It does a deep copy of all properties from source to destination preserving properties in child objects. Note how we can also use multiple source objects that will be merged in order:

const person1 = {
  name: 'Leo',
  address: {
    description: 'Oxford Street'
  }
}
const person2 = {
  id: 1,
  address : {
    postcode: 'SW1'
  }
}
const merged = angular.merge(person1, person2); // ALL the similar WILL PREVAIL
// merged object
// {id: 1, name:'Leo', address:{description:'Oxford Street',postcode: 'SW1'}}

const extended = angular.extend(person1, person2); // replace similar properties
// extended object
// {id: 1, name:'John', address:{postcode:'SW1'}}
Enter fullscreen mode Exit fullscreen mode

{ 'Leo Lanese',
'Building Inspiring Responsive Reactive Solutions',
'London, UK' }
Portfolio http://www.leolanese.com
Twitter: twitter.com/LeoLaneseltd
Questions / Suggestion / Recommendation ? developer@leolanese.com
DEV.to: www.dev.to/leolanese
Blog: leolanese.com/blog

Discussion (0)