Understanding Javascript Shallow Copy Behavior

Last modified: 
Friday, July 27th, 2018
Topics: 
Javascript

These are my notes on Javascript's behavior when creating copies of objects with the ES6 spread operator (myCopy = {...myOriginal}), Object.assign({}, myOriginal), and destructuring (const {var1, var2, var3} = myOriginal). The most recent version of these notes will be on JSBin at https://Erik-Codeblind.jsbin.com/wexikur/edit?js,console . I'm keeping this big hunk here to make it easier for people to find.

/*

This is a demonstration of what a "shallow copy" means in JavaScript. 

It is intended to show some gotchas when destructuring and copying
data structures in the name of immutability.

*/

// This is the source object we will create our copies from. 
const x = {
  num: 1,
  bool: true,
  fruit: 'banana',
  data: [1, 2, 3, 4],
  obj: {
    color: 'red',
    moreData: ['a', 'b', 'c']
  },
  func: function() {
    return 'original function';
  }
};

// y.data and y.obj will be references shared by x, y, z, and za.
const y = {...x};

// z.data and z.obj will also be references shared by x, y, z, and za.
const z = Object.assign({}, y);

// za.data and za.obj are references shared by x, y, z, and za.
const za = {
  num: x.num,
  bool: x.bool,
  fruit: y.fruit,
  data: z.data,
  color: x.obj.color,
  moreData: x.obj.moreData
};

// zb.data is a copy of x.data with its own space in memory.
// It is not shared by any of our other objects.
const zb = {
  data: [...x.data]
};

// Don't let the fact we're setting variables inside objects 
// confuse you. Any variable assigned directly to an array or 
// object is only a reference.
const zc = x.data;

// Destructuring x still results in data being a reference.
const {data} = x;

// When the value of a function return is a reference, it's a reference!
function getXData() {
  return x.data;
}

// xData is a reference to x.data and it's value will
// change if x.data is modified, even if we never 
// again call the getXData() function.
const xData = getXData(); 

// Generate a section header.
const sec = name => {
  name = (name.length % 2 == 0) ? name : name + ' ';
  const pad = '/'.repeat((45 - name.length) / 2);
  const str = `${pad} ${name.toUpperCase()} ${pad}`; 
  console.log(str);
};

// Strings, Numbers, Booleans, and Functions are copied completely.

// x.fruit, z.fruit, and za.fruit will not be modified because they
// each have their own unique memory spaces.
sec('Strings');
y.fruit = 'apple';

console.log(x.fruit); // => banana 
console.log(y.fruit); // => apple
console.log(z.fruit); // => banana
console.log(za.fruit); // => banana

sec('More Strings');
// You can change any of them without mutating the others.
z.fruit = 'pear';
za.fruit = 'cherry';
x.fruit = 'peach';

console.log(x.fruit); // => peach 
console.log(y.fruit); // => apple
console.log(z.fruit); // => pear
console.log(za.fruit); // => cherry

// The same is true for numbers and booleans.
sec('Numbers');
z.num = 2;

console.log(x.num); // => 1 
console.log(y.num); // => 1
console.log(z.num); // => 2
console.log(za.num) // => 1

sec('Booleans')
za.bool = false;

console.log(x.bool); // => true 
console.log(y.bool); // => true
console.log(z.bool); // => true
console.log(za.bool) // => false

// And also for function definitions.
sec('Function Definitions')
y.func = () => 'overloaded function';

console.log(x.func()); // => original function
console.log(y.func()); // => overloaded function
console.log(z.func()); // => original function

// Arrays and Objects are not copied to a new memory 
// location, only their pointers are copied.

// We previously set xData using the return value of getXData(),
// but this will change, even if we never call getXData() again.
sec('xData Start Value');
console.log(xData); // => [1, 2, 3, 4]

// Updating y.data also updates x, z, za, and zc because only
// the pointer has been copied. All these objects are literally sharing 
// the same exact array in memory.
sec('Arrays Are References');
y.data.push('shared memory allocation');

console.log(x.data); // => [1, 2, 3, 4, "shared allocation in memory"]
console.log(y.data); // => [1, 2, 3, 4, "shared allocation in memory"]
console.log(z.data); // => [1, 2, 3, 4, "shared allocation in memory"]
console.log(za.data); // => [1, 2, 3, 4, "shared allocation in memory"]
console.log(zc); // console.log(za.data); // => [1, 2, 3, 4, "shared allocation in memory"]

// Note that destructuring `{data} = x` still results in a pointer.
sec('Destructuring Will Not Save You');
console.log(data); // => [1, 2, 3, 4, "shared allocation in memory"]

// It doesn't matter that we already assigned a value to xData, because the 
// return value of getXData() was a reference.
sec('xData Is a Reference!');
console.log(xData); // => [1, 2, 3, 4, "shared allocation in memory"]

// zb.data, on the other hand, was created by using spread on the 
// array to create a copy, so it is not be mutated by changes to
// original array. (See note below.)
sec('Copy With Spread');
console.log(zb.data); // => [1, 2, 3, 4]

// Note that if the x.data array had contained objects or arrays, those
// items would be references themselves. 

// You can test whether two arrays (or objects) point to the same
// reference by using the strict equality operator (===). 
sec('Strict Equality Checks');
const arr = [1, 2, 3, 4];

console.log(xData === x.data); // => true
console.log([1, 2, 3] == [1, 2, 3]); // => false
console.log(zb.data === arr) // => false

// Comparing that two arrays are identical in content and order
console.log(JSON.stringify([1, 2, 3]) === JSON.stringify([1, 2, 3])); // => true
console.log(JSON.stringify(zb.data) === JSON.stringify(arr)); // => true

// Reference behavior is the same for objects...
sec('Objects are References');
x.obj.color = 'blue';

console.log(x.obj.color); // => blue
console.log(y.obj.color); // => blue
console.log(z.obj.color); // => blue

// x.obj.color resolves to a string, so we get
// a new memory allocation at za.color!
console.log(za.color); // => red 

sec('And So On...')
// x.obj.moreData resolves to an array, so the pointer is shared.
za.moreData.push('and so on, and so on...');

console.log(x.obj.moreData); // => ["a", "b", "c", "and so on, and so on..."]
console.log(y.obj.moreData); // => ["a", "b", "c", "and so on, and so on..."]
console.log(z.obj.moreData); // => ["a", "b", "c", "and so on, and so on..."]
console.log(za.moreData); // => ["a", "b", "c", "and so on, and so on..."]


The operator of this site makes no claims, promises, or guarantees of the accuracy, completeness, originality, uniqueness, or even general adequacy of the contents herein and expressly disclaims liability for errors and omissions in the contents of this website.