Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active February 7, 2020 01:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/85e1f2d708d3728afab239ab26ea11ce to your computer and use it in GitHub Desktop.
Save dfkaye/85e1f2d708d3728afab239ab26ea11ce to your computer and use it in GitHub Desktop.
Immutability in JavaScript

Short Attention Span Theater

Gratuitous introductory section you can skip

In certain programming languages, some values are said to be "immutable," meaning that once assigned to a variable name, they cannot be changed in part.

var s = 'string';
s += ' bean';

s will still be 'string' even though we tried to alter it with the concatenation operator into 'string bean'.

That applies to ALL values. Unless you save the result of the operation into another variable name, the result does not persist.

With objects (wrapped natives like String, Object, Array, and Date), however, the object acts as a collection of methods that operate on an internal state (shorthand term meaning collection of values).

An array's state can be modified by several methods, such as reverse, sort, push, pop, shift, splice, etc.

var a = [3, 2, 1];
a.reverse();

a will now render as [1, 2, 3].

Other array methods, such as concat and slice, return a new Array. Unless you save the result of concat and slice, the result does not persist.

Immutability means returning a new object

Sometimes we want that, sometimes we don't.

In general, but not always, it is advisable to pass values to functions and work on copies of these values so that we don't introduce side-effects by accident.

JavaScript does not have a one-stop cloning method or syntax for objects of any type. (Hold your objections about JSON.stringify and JSON.parse for a moment.) For objects and arrays you can use the spread syntax to create new objects, {...object}, or arrays, [...array] whereas other objects have their own internal state that cannot be cloned via the spread syntax. For that reason, I avoid the spread syntax and use type-specific methods.

var o = { name: 'test' }
var a = ['2', 3, 'a', 1, 0];
var d = new Date('2019/2/1');

function obj(o) {
  return Object.assign({}, o);
}

function arr(a) {
  return a.slice();
}

function date(d) {
  return new Date(typeof d.toISOString == 'function' ? d.toISOString() : d);
}

console.log(obj(o));
// {name: "test"}
console.log(obj(o) !== o);
// true

console.log(arr(a));
//  ["2", 3, "a", 1, 0]
console.log(arr(a) !== a);
// true

console.log(date(d));
// Fri Feb 01 2019 00:00:00 GMT-0800 (Pacific Standard Time)
console.log(date(d) !== d);
// true

// Note that as Date objects can be tricky, it is better to create date strings instead (they're immutable!) and construct Date objects from those once you really need them.

var now = Date.now()
var anHourAgo = now - (60 * 60 * 1000); // minutes, seconds, milliseconds

console.log(new Date(now));
// Thu Feb 06 2020 17:19:10 GMT-0800 (Pacific Standard Time)

console.log(new Date(anHourAgo));
// Thu Feb 06 2020 16:19:10 GMT-0800 (Pacific Standard Time)

console.log(date(anHourAgo) !== date(anHourAgo));
// true

DOM methods to do that

First let's create a test element:

var e = document.createElement('div');

e.textContent = 'test element';
e.setAttribute('id', 'hello')

document.body.appendChild(e);

Now we'll find it by selector and and make a deep copy of it:

function clone(selector) {
  var clone = document.querySelector(selector);
  
  return clone
    // If it exists, use the cloneNode(deep) method;
    ? clone.cloneNode(true)
    // if not, return an error-like message.
    : { message: `no matching element for selector "${ selector }"` };
}

var a = clone('#hello');
var b = clone('#hello');

console.log( a !== e );
console.log( a !== b );
console.log( b !== e );
// should all print true 

What was that about JSON.stringify and JSON.parse…?

Some have suggested that an object clone method can be composed of the JSON methods, as follows:

function clone(object) {
    return JSON.parse(JSON.stringify(source));
}

var result = clone({
    key: undefined,
    send: function () { return "send method called" }
});

console.log(result);

However, JSON is not an object type but a data format. The result of the above operation is to return an object without a key field (because the value is undefined), or a send method (because methods/functions are not allowed). An array with a custom property (e.g., array.hello = "hello") will be converted to an array without that property.

For more details, see this article by Petro Zubar (2019) → https://medium.com/@pmzubar/why-json-parse-json-stringify-is-a-bad-practice-to-clone-an-object-in-javascript-b28ac5e36521

To be continued...

Maybe.

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