Clone an Object with Closures

Background: Object with closure properties

Closure properties can be thought of as a non-persistent property e.g. a web-page with thumbnail of cars and user selects a car with mouse click. Let’s say we have a isSelected closure property.

function Car() {
  // Instance property
  this.name = "Jeep";
  
  // Closure property
  var _isSel = false;
  this.isSelected = function(val) {
    if (val != null) {
      _isSel = val;
    }
    return _isSel;
  }
}

var car1 = new Car();
car1.isSelected(true);   // Closure property setter
car1.isSelected();       // Closure property accessor

Using this approach we can have behavior similar to property, but without saving it. Car object has only name property.

JSON.stringify(car1);           // "{"name":"Jeep"}"

Copying an Object

a) Shallow copy, angular.extend(dst, src)

Approach: Duplicate as little as possible i.e. copy actual value of primitive fields and reference address of reference/object fields.

var obj1 = {
  "id": "abcd",
  "car": {
    "name": "default",
    "year": "2013",
   }
};

var obj2 = {
  "id": "1234",
  "hash": "!@QWER12"
};

// Copy everything from source to destination, 
// overwrite if same property exists at destination
angular.extend(obj2, obj1)

// obj2 has everything copied from obj1.
{
  "id": "abcd",   // id overwritten by obj1's id
  "hash":"!@QWER12",  // NO CHANGE on obj2 property which doesn't exist on obj1
  "car": {
    "name": "default",
    "year": "2013"
  }
}

// But it's a SHALLOW copy i.e. both obj1 and obj2 points to same car. 
// Changing obj1's car property will update both objects.
obj1.car.name = "Jeep";
console.log(obj1.car.name);   // "Jeep"
console.log(obj2.car.name);   // "Jeep"

b) Deep copy, angular.copy(src, dst)

Approach: Duplicate everything i.e. copy actual value of primitive fields and create new object of reference/object fields with same data.

var obj1 = {
  "id": "abcd",
  "car": {
    "name": "default",
    "year": "2013",
   }
};

var obj2 = {
  "id": "1234",
  "hash": "!@QWER12"
};

// Create duplicate copy of everything in source on destination
// angular.copy(src,dst) 
//    deletes everything at destination first and create clone of source.
// angular.merge(dst,src) 
//    keeps existing copy property at destination and add duplicate of source

angular.copy(obj1, obj2)

// obj2 has everything copied from obj1.
{
  "id": "abcd",   // id overwritten by obj1's id. NO hash property
  "car": {
    "name": "default",
    "year": "2013"
  }
}

// It's a DEEP copy i.e. obj1 and obj2 reference to different car. 
// Changing obj1's car property will NOT update other object.
obj2.car.name = "Jeep";
console.log(obj1.car.name);   // "default"
console.log(obj2.car.name);   // "Jeep"

c) Closure copy

Copying a closure is a bit tricky because closure variables are private and they are not copied by any of the function: merge, extend, copy. If you don’t handle them carefully, you’ll end up with multiple objects pointing to same data. In our car class, using extend method to deep copy an object we will have all objects sharing same isSelected.

function Car() {
  // Instance property
  this.name = "default";
  
  // Closure property
  var _isSel = false;
  this.isSelected = function(val) {
    if (val != null) {
      _isSel = val;
    }
    return _isSel;
  }
}

var a = new Car();
var b = angular.copy(a);  // Create a deep copy of a. 
// a and b share same closure 'isSelected()'

console.log(b.isSelected());  // false
a.isSelected(true);   
console.log(b.isSelected()); // true. expected false

console.log(b.name);          // "default"
a.name = "Jeep";
console.log(b.name);         // "default"
console.log(a.name);         // "Jeep"

A possible solution or hack is to separate out closure property from the actual object and new it everytime you need to create a copy of it.

var a = new Car();
var b = new Car();  

console.log(b.isSelected());  // false
a.isSelected(true);
console.log(b.isSelected()); // false
console.log(a.isSelected()); // true

// For copying instance properties, we can use angular.merge. 
// However things gets complicated when we've nested objects with closure.

// cloning an object with closure
Car.protoype.cloneCarFrom = function (origCar) {
  var propertyNotToBeCloned = { isSelected: this.isSelected };  
  
  var newCar = angular.merge({}, origCar, propertyNotToBeCloned);
  return newCar;
}

Credits:

  1. AngularJS: copy vs extend
  2. SO: deep merge objects
  3. SO: deep copy vs shallow copy

P.S.: Closure property term isn’t official, instead used here just for demo.