This blog post answers the following questions:

  • What is shared mutable state?
  • Why is it a problem?
  • How to avoid it? The section labeled “(advance)” goes further and can be skipped if you want to read this blog post faster.

The main content

  1. What is shared mutable state and why is it a problem?
  2. Avoid sharing by copying data
    • Shallow copy vs. deep copy
    • Shallow copy in JavaScript
    • JavaScript deep Copy
    • How does replication help share mutable state?
  3. Mutation is avoided by non-destructive updating
    • Background: Destructive updates versus non-destructive updates
    • How do non-destructive updates help share mutable state?
  4. Mutation is prevented by making the data immutable
    • Background :JavaScript immutability
    • Immutable wrapper (Advance)
    • How does immutability help share mutable state?
  5. Avoid libraries that share mutable state
    • Immutable.js
    • Immer
  6. thanks
  7. Further reading

1 What is shared mutable state and why is it a problem?

Sharing mutable state works as follows:

  • If two or more parts can change the same data (variables, objects, etc.)
  • If their life cycles overlap, then, there is a risk that one party’s modifications prevent the other from working correctly. Here’s an example:
function logElements(arr) {
  while(arr.length > 0) { console.log(arr.shift()); }}function main() {
  const arr = ['banana'.'orange'.'apple'];

  console.log('Before sorting:');
  logElements(arr);

  arr.sort(); // changes arr

  console.log('After sorting:');
  logElements(arr); // (A)
}
main();

// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
Copy the code

There are two separate parts: the function logElements() and the function main(). The latter wants to record an array before and after the sort. However, it uses logElements() to clear the parameters. Therefore, main() records an empty array at line A.

In the rest of this article, we’ll discuss three ways to avoid the shared mutable state problem:

  • Avoid sharing by copying data

  • Mutation is avoided by non-destructive updating

  • Mutation is prevented by making the data immutable

Next, we’ll go back to the example we just saw and fix it.

2 Avoid sharing by copying data

Before we discuss how replication avoids sharing, we need to look at how to copy data in JavaScript.

2.1 Shallow copy vs. Deep Copy

Data replication has two “depths” :

  • Shallow copy copies only the top-level items of objects and arrays. The entry value remains the same when it is original and copied.
  • Deep copy also copies the value of the entry, except that it traverses the entire tree from the root node and copies all the nodes.

Both types of replication are described in the next section. Unfortunately, JavaScript only has built-in support for shallow copies. If we need deep copy, we need to implement it ourselves.

2.2 Shallow copy in JavaScript

Let’s look at some simple ways to copy data.

2.2.1 Copying ordinary objects and arrays by extension

We can extend this to object literals and array literals:

const copyOfObject = {... originalObject}; const copyOfArray = [...originalArray];Copy the code

However, there are several limitations to extended replication:

  • The prototype was not copied:
class MyClass {}

const original = new MyClass();
assert.equal(MyClass.prototype.isPrototypeOf(original), true); const copy = {... original}; assert.equal(MyClass.prototype.isPrototypeOf(copy),false);
Copy the code
  • Special objects, such as regular expressions and dates, have “internal slots” with special attributes that are not copied

  • Copy only your own (non-inherited) properties. Considering how the prototype chain works, this is usually the best approach. But you still need to be aware of it. In the following example, inheritedProp is not available in copy because we only copy our own properties and don’t keep the stereotype.

const proto = { inheritedProp: 'a' };
const original = {__proto__: proto, ownProp: 'b' };
assert.equal(original.inheritedProp, 'a');
assert.equal(original.ownProp, 'b'); const copy = {... original}; assert.equal(copy.inheritedProp, undefined); assert.equal(copy.ownProp,'b');
Copy the code
  • Only enumerable properties are copied. For example, the array instance’s own property. Length is not enumerable and cannot be copied:
const arr = ['a'.'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true); const copy = {... arr}; assert.equal({}.hasOwnProperty.call(copy,'length'), false);
Copy the code
  • Property independent of property, its copy will always be a writable and configurable data property – for example:
const original = Object.defineProperties({}, {
  prop: {
    value: 1,
    writable: false,
    configurable: false,
    enumerable: true,}}); assert.deepEqual(original, {prop: 1}); const copy = {... original}; // Attributes `writable` and `configurable` of copy are different: assert.deepEqual(Object.getOwnPropertyDescriptors(copy), { prop: { value: 1, writable:true,
    configurable: true,
    enumerable: true,}});Copy the code

This means that getters and setters are not faithfully copied either: property values (for data properties), get(for getters), and set(for setters) are independent.

const original = {
  get myGetter() { return123},setmySetter(x) {}, }; assert.deepEqual({... original}, { myGetter: 123, // not a getter anymore! mySetter: undefined, });Copy the code
  • Replication is shallow: replication has a new version of each key-value entry in the original data, but the value of the original data itself is not copied. Such as:
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = {... original}; // Property .name is a copy copy.name ='John';
assert.deepEqual(original,
  {name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(copy,
  {name: 'John', work: {employer: 'Acme'}});

// The value of .work is shared
copy.work.employer = 'Spectre';
assert.deepEqual(
  original, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(
  copy, {name: 'John', work: {employer: 'Spectre'}});
Copy the code

Some restrictions can be removed, others cannot:

  • We can give the duplicate the same prototype during the copying process:
class MyClass {} const original = new MyClass(); const copy = { __proto__: Object.getPrototypeOf(original), ... original, }; assert.equal(MyClass.prototype.isPrototypeOf(copy),true);
Copy the code

Alternatively, we can set the prototype of the replica via Object.setProtoTypeof () after the replica is created.

  • There is no easy way to copy special objects.
  • As mentioned earlier, copying only your own attributes is a feature, not a limitation.
  • We can use the Object. GetOwnPropertyDescriptors () and Object defineProperties () to copy objects (hereafter will explain how to do this) :
    • They consider all properties (not just values), so they correctly copy getters, setters, read-only properties, and so on.
    • Enumerable Object. Getownpropertydescriptors () both retrieval attributes, also retrieve an enumerated attribute.
  • We will discuss deep replication later in this article.

2.2.2 Shallow Replication using Object.assign() (advance)

Assign () works much like extending objects into objects. In other words, the following two replication methods are basically the same:

const copy1 = {... original}; const copy2 = Object.assign({}, original);Copy the code

The advantage of using methods instead of syntax is that it can be populated on older JavaScript engines through libraries.

However, object.assign () isn’t exactly like spread. It has one subtle difference: it creates attributes differently.

  • Assign () Creates the attribute of the copy using assign.
  • Extensions define new properties in the copy.

In other respects, assignments call their own and inherited setters, while definitions (here, extensions) do not call (more about assignments and definitions). This difference is rarely noticed. The following code is an example, but it is artificial:

const original = {['__proto__']: null}; const copy1 = {... original}; // copy1 has the own property'__proto__'
assert.deepEqual(
  Object.keys(copy1), ['__proto__']);

const copy2 = Object.assign({}, original);
// copy2 has the prototype null
assert.equal(Object.getPrototypeOf(copy2), null);
Copy the code

2.2.3 through Object. GetOwnPropertyDescriptors () and the Object. DefineProperties () is a shallow copy (advance)

JavaScript allows you to create properties through property descriptors, which are objects that specify property properties. We’ve seen it in action, for example, with Object.defineProperties(). If we put this method and the Object. GetOwnPropertyDescriptors (), we can more faithfully reproduce:

function copyAllOwnProperties(original) {
  return Object.defineProperties(
    {}, Object.getOwnPropertyDescriptors(original));
}
Copy the code

This removes two limitations on copying objects by extension.

First, copy all properties of your property correctly. So we can now copy our own getters and setters:

const original = {
  get myGetter() { return123},set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);
Copy the code

Secondly, as a result of the Object. GetOwnPropertyDescriptors (), which cannot be enumerated attribute also be copied:

const arr = ['a'.'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);

const copy = copyAllOwnProperties(arr);
assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
Copy the code

2.3 JavaScript deep copy

Now it’s time to tackle deep replication. First, we’ll manually deep copy, and then we’ll examine the common methods.

2.3.1 Manual Deep Replication through Nested Extension

If we nest the extension, we get a deep copy:

const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = {name: original.name, work: {... original.work}}; // We copied successfully: assert.deepEqual(original, copy); // The copy is deep: assert.ok(original.work ! == copy.work);Copy the code

This is a HACK method, but when push comes to shove, it provides a quick solution: To deeply copy an object in its original form, we first convert it to a JSON string and parse that JSON string:

function jsonDeepCopy(original) {
  return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
Copy the code

The significant disadvantage of this approach is that we can only copy the properties of keys and values supported by JSON.

Some unsupported keys and values are simply ignored:

assert.deepEqual(
  jsonDeepCopy({
    [Symbol('a')]: 'abc',
    b: function () {},
    c: undefined,
  }),
  {} // empty object
);
Copy the code

Exceptions for other reasons:

assert.throws(
  () => jsonDeepCopy({a: 123n}),
  /^TypeError: Do not know how to serialize a BigInt$/);
Copy the code

2.3.3 Implementing Universal Deep Replication

The following functions generally make a deep copy of the original value:

function deepCopy(original) {
  if (Array.isArray(original)) {
    const copy = [];
    for (const [index, value] of original.entries()) {
      copy[index] = deepCopy(value);
    }
    return copy;
  } else if (typeof original === 'object'&& original ! == null) { const copy = {};for (const [key, value] of Object.entries(original)) {
      copy[key] = deepCopy(value);
    }
    return copy;
  } else {
    // Primitive value: atomic, no need to copy
    returnoriginal; }}Copy the code

This function handles three cases:

  • If original is an array, we create a new array and deeply copy the original elements into it.
  • If Original is an object, we use a similar approach.
  • If original is a raw value, we don’t need to do anything.

Let’s try deepCopy():

const original = {a: 1, b: {c: 2, d: {e: 3}}}; const copy = deepCopy(original); // Are copy and original deeply equal? assert.deepEqual(copy, original); // Did we really copy all levels // (equal content, but different objects)? assert.ok(copy ! == original); assert.ok(copy.b ! == original.b); assert.ok(copy.b.d ! == original.b.d);Copy the code

Note that deepCopy() fixes only one extension problem: shallow copy. Everything else remains the same: stereotypes are not copied, special objects are only partially copied, non-enumerable attributes are ignored, and most attribute attributes are ignored.

It’s often impossible to achieve complete replication: not all data is a tree, sometimes you don’t want all attributes, and so on.

A cleaner version of deepCopy()

If we use.map() and object.fromentries (), we can make the previous deepCopy() implementation simpler:

function deepCopy(original) {
  if (Array.isArray(original)) {
    return original.map(elem => deepCopy(elem));
  } else if (typeof original === 'object'&& original ! == null) {return Object.fromEntries(
      Object.entries(original)
        .map(([k, v]) => [k, deepCopy(v)]));
  } else {
    // Primitive value: atomic, no need to copy
    returnoriginal; }}Copy the code

2.3.4 Implementing Deep Replication in A Class (Advance)

Two techniques are commonly used to achieve deep copy of class instances:

  • The clone () method
  • Copy constructor

.clone() methods

This technique introduces a.clone() method for each class that wants to deeply copy an example. It returns a deep copy. The following example shows three classes that can be cloned.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  clone() {
    return new Point(this.x, this.y);
  }
}
class Color {
  constructor(name) {
    this.name = name;
  }
  clone() {
    return new Color(this.name);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  clone() {
    return new ColorPoint(
      this.x, this.y, this.color.clone()); // (A)
  }
}
Copy the code

Line A demonstrates an important aspect of this technique: compound instance property values must also be recursively cloned.

Static factory method

The copy constructor is a constructor that uses another instance of the current class to set the current instance. Copy constructors are popular in static languages such as c++ and Java, where multiple versions of the constructor can be provided through static overloading (static means happening at compile time).

In JavaScript, you can do this (but not very elegantly):

class Point { constructor(... args) {if (args[0] instanceof Point) {
      // Copy constructor
      const [other] = args;
      this.x = other.x;
      this.y = other.y;
    } else{ const [x, y] = args; this.x = x; this.y = y; }}}Copy the code

You can use this class like this:

const original = new Point(-1, 4);
const copy = new Point(original);
assert.deepEqual(copy, original);
Copy the code

In contrast, static factory methods work better in JavaScript (static means they are class methods).

In the following example, the three classes Point, Color, and ColorPoint all have a static factory method.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  static from(other) {
    return new Point(other.x, other.y);
  }
}
class Color {
  constructor(name) {
    this.name = name;
  }
  static from(other) {
    return new Color(other.name);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  static from(other) {
    return new ColorPoint(
      other.x, other.y, Color.from(other.color)); // (A)
  }
}
Copy the code

In line A, we use recursive copying again.

This is how colorPoint.from () works:

const original = new ColorPoint(-1, 4, new Color('red'));
const copy = ColorPoint.from(original);
assert.deepEqual(copy, original);
Copy the code

2.4 How does replication help share mutable state?

As long as we only read from the shared state, we shouldn’t have any problems. Before we modify it, we need to “unshare” it, by copying it (as deeply as possible).

Defensive copying is a technique for copying when something could go wrong. Its goal is to keep current entities (functions, classes, and so on) safe

  • Input: Duplicates (potentially) shared data passed to us, allowing us to use it without interference from external entities.
  • Output: Copying internal data before exposing it to an external party means that party cannot disrupt our internal activities.

Note that these measures protect us from other parties, but they also protect other parties from us.

The next section demonstrates both types of defensive replication.

2.4.1 Copying Share Input

Remember that in the excitation example at the beginning of this article, we ran into trouble because logElements() modified its arr argument:

function logElements(arr) {
  while(arr.length > 0) { console.log(arr.shift()); }}Copy the code

Let’s add defense copy to this function:

function logElements(arr) {
  arr = [...arr]; // defensive copy
  while(arr.length > 0) { console.log(arr.shift()); }}Copy the code

Now logElements() no longer causes problems if it is called main():

function main() {
  const arr = ['banana'.'orange'.'apple'];

  console.log('Before sorting:');
  logElements(arr);

  arr.sort(); // changes arr

  console.log('After sorting:');
  logElements(arr); // (A)
}
main();

// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
// 'apple'
// 'banana'
// 'orange'

Copy the code

2.4.2 Copying public internal Data

Let’s start with A StringBuilder class that doesn’t copy its public internal data (line A):

class StringBuilder {
  constructor() {
    this._data = [];
  }
  add(str) {
    this._data.push(str);
  }
  getParts() {
    // We expose internals without copying them:
    return this._data; // (A)
  }
  toString() {
    return this._data.join(' '); }}Copy the code

As long as you don’t use.getParts(), everything works fine:

const sb1 = new StringBuilder();
sb1.add('Hello');
sb1.add(' world! ');
assert.equal(sb1.toString(), 'Hello world! ');
Copy the code

However, if the result of.getParts() changes (line A), the StringBuilder will stop working properly:

const sb2 = new StringBuilder();
sb2.add('Hello');
sb2.add(' world! ');
sb2.getParts().length = 0; // (A)
assert.equal(sb2.toString(), ' '); // not OK
Copy the code

The solution is to defensively copy internal before exposure._data (line A):

class StringBuilder {
  constructor() {
    this._data = [];
  }
  add(str) {
    this._data.push(str);
  }
  getParts() {
    // Copy defensively
    return [...this._data]; // (A)
  }
  toString() {
    return this._data.join(' '); }}Copy the code

Now, the result of changing.getParts() no longer interferes with sb’s operation:

const sb = new StringBuilder();
sb.add('Hello');
sb.add(' world! ');
sb.getParts().length = 0;
assert.equal(sb.toString(), 'Hello world! '); // OK
Copy the code

Mutation is avoided by nondestructive renewal

We will first explore the difference between destructive and non-destructive data updates. Then we’ll learn how nondestructive updates can avoid mutations.

3.1 Background: Destructive and non-destructive updates

We can distinguish between two different data update methods:

  • Disruptive updates to the data can cause changes to the data, resulting in the required form.
  • A non-destructive update of the data creates a copy of the data with the required form. The latter approach is similar to making a copy first and then destructively changing it, but both are done simultaneously.

3.1.1 Example: Update objects destructively and non-destructively

This is how we destructively set the properties of an object.

const obj = {city: 'Berlin', country: 'Germany'};
const key = 'city';
obj[key] = 'Munich';
assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
Copy the code

The following functions change properties nondestructively:

function setObjectNonDestructively(obj, key, value) {
  const updatedObj = {};
  for (const [k, v] of Object.entries(obj)) {
    updatedObj[k] = (k === key ? value : v);
  }
  return updatedObj;
}
Copy the code

It can be used as follows:

const obj = {city: 'Berlin', country: 'Germany'};
const updatedObj = setObjectNonDestructively(obj, 'city'.'Munich');
assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'});
assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});
Copy the code

The extension makes setobjectnondestrucative() more concise:

function setObjectNonDestructively(obj, key, value) {
  return{... obj, [key]: value}; }Copy the code

Note: Both versions of Setobjectnondestrucative () update are shallow.

3.1.2 Example: Update arrays destructively and non-destructively

Here’s how we destructively set the elements of an array:

const original = ['a'.'b'.'c'.'d'.'e'];
original[2] = 'x';
assert.deepEqual(original, ['a'.'b'.'x'.'d'.'e']);
Copy the code

A non-destructive update array is much more complex than a non-destructive update object.

function setArrayNonDestructively(arr, index, value) {
  const updatedArr = [];
  for (const [i, v] of arr.entries()) {
    updatedArr.push(i === index ? value : v);
  }
  return updatedArr;
}

const arr = ['a'.'b'.'c'.'d'.'e'];
const updatedArr = setArrayNonDestructively(arr, 2, 'x');
assert.deepEqual(updatedArr, ['a'.'b'.'x'.'d'.'e']);
assert.deepEqual(arr, ['a'.'b'.'c'.'d'.'e']);
Copy the code

.slice() and spread make setarraynondestructive() more concise:

function setArrayNonDestructively(arr, index, value) {
  return [
  ...arr.slice(0, index), value, ...arr.slice(index+1)]
}
Copy the code

Note: Both versions of setarraynondestrucsive() are shallow updates.

3.1.3 Manual Deep Update

So far, we have only roughly updated the data. Let’s deal with deep updates. The following code demonstrates how to do this manually. We are in the process of changing names and employers.

const original = {name: 'Jane', work: {employer: 'Acme'}}; const updatedOriginal = { ... original, name:'John', work: { ... original.work, employer:'Spectre'}}; assert.deepEqual( original, {name:'Jane', work: {employer: 'Acme'}});
assert.deepEqual(
  updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
Copy the code

3.1.4 Implement universal deep update

The following functions implement general depth updates.

function deepUpdate(original, keys, value) {
  if (keys.length === 0) {
    return value;
  }
  const currentKey = keys[0];
  if (Array.isArray(original)) {
    return original.map(
      (v, index) => index === currentKey
        ? deepUpdate(v, keys.slice(1), value) // (A)
        : v); // (B)
  } else if (typeof original === 'object'&& original ! == null) {return Object.fromEntries(
      Object.entries(original).map(
        (keyValuePair) => {
          const [k,v] = keyValuePair;
          if (k === currentKey) {
            return [k, deepUpdate(v, keys.slice(1), value)]; // (C)
          } else {
            return keyValuePair; // (D)
          }
        }));
  } else {
    // Primitive value
    returnoriginal; }}Copy the code

If we treat the value as the root of the tree being updated, deepUpdate() only makes deep changes to A single branch (lines A and C), and copies shallower to all the other branches (lines B and D).

This is what it looks like using deepUpdate() :

const original = {name: 'Jane', work: {employer: 'Acme'}};

const copy = deepUpdate(original, ['work'.'employer'].'Spectre');
assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});
Copy the code

3.2 How do non-destructive updates help share mutable state?

With non-destructive updates, sharing data is not a problem because we never change the shared data. (Obviously, this only works if all parties do it.)

Interestingly, copying data becomes very simple:

const original = {city: 'Berlin', country: 'Germany'};
const copy = original;
Copy the code

Actual copies of originals will only be made if necessary and if we are making non-destructive changes.

Mutation is prevented by making data immutable

We can prevent mutations in shared data by making it immutable. Next, we’ll look at how JavaScript supports immutability. Then, we’ll discuss how immutable data can help share mutable state.

4.1 Background :JavaScript immutability

JavaScript has three layers of protected objects:

  • Preventing extensions makes it impossible to add new attributes to an object. However, you can still delete and change properties.
    • Methods: Object. PreventExtensions (obj)
  • Sealing prevents scaling and makes all properties unconfigurable (roughly: you can no longer change how properties work).
    • Methods: Object. Seal (obj)
  • After freezing an object, make all its properties unwritable. That is, objects are not extensible, and all properties are read-only and cannot be changed.
    • Methods: Object. Freeze (obj)

Since we want our objects to be completely immutable, we’ll just use object.freeze () in this blog post.

4.1.1 Freezing is shallow

Object.freeze(obj) Freezes only obj and its properties. It does not freeze the values of these attributes – for example:

const teacher = {
  name: 'Edna Krabappel',
  students: ['Bart']}; Object.freeze(teacher); assert.throws( () => teacher.name ='Elizabeth Hoover',
  /^TypeError: Cannot assign to read only property 'name'/);

teacher.students.push('Lisa');
assert.deepEqual(
  teacher, {
    name: 'Edna Krabappel',
    students: ['Bart'.'Lisa']});Copy the code

4.1.2 Deep Freezing

If we want a deep freeze, we need to implement it ourselves:

function deepFreeze(value) {
  if (Array.isArray(value)) {
    for (const element of value) {
      deepFreeze(element);
    }
    Object.freeze(value);
  } else if (typeof value === 'object'&& value ! == null) {for (const v of Object.values(value)) {
      deepFreeze(v);
    }
    Object.freeze(value);
  } else {
    // Nothing to do: primitive values are already immutable
  } 
  return value;
}
Copy the code

Revisiting the example from the previous section, we can check if deepFreeze() is really deep:

const teacher = {
  name: 'Edna Krabappel',
  students: ['Bart']}; deepFreeze(teacher); assert.throws( () => teacher.name ='Elizabeth Hoover',
  /^TypeError: Cannot assign to read only property 'name'/);

assert.throws(
  () => teacher.students.push('Lisa'),
  /^TypeError: Cannot add property 1, object is not extensible$/);
Copy the code

4.2 Immutable Wrapper (Advance)

Immutable wrappers wrap mutable collections and provide the same API, but without destructive operations. Now, for the same collection, we have two interfaces: one is mutable and the other is immutable. This is very useful when we need to securely expose mutable internal data.

The next two sections show wrappers for maps and arrays. Both have the following limitations:

  • They are sketches. More needs to be done to make them fit for practical use: better checks, support for more methods, and so on.
  • They work in a shallow way.

4.2.1 Immutable wrappers for maps

The ImmutableMapWrapper class generates a wrapper for a Map:

class ImmutableMapWrapper {
  constructor(map) {
    this._self = map;
  }
}

// Only forward non-destructive methods to the wrapped Map:
for (const methodName of ['get'.'has'.'keys'.'size']) {
  ImmutableMapWrapper.prototype[methodName] = function(... args) {return this._self[methodName](...args);
  }
}
Copy the code

The following is an example:

const map = new Map([[false.'no'], [true.'yes']]);
const wrapped = new ImmutableMapWrapper(map);

// Non-destructive operations work as usual:
assert.equal(
  wrapped.get(true), 'yes');
assert.equal(
  wrapped.has(false), true);
assert.deepEqual(
  [...wrapped.keys()], [false.true]);

// Destructive operations are not available:
assert.throws(
  () => wrapped.set(false.'never! '),
  /^TypeError: wrapped.set is not a function$/);
assert.throws(
  () => wrapped.clear(),
  /^TypeError: wrapped.clear is not a function$/);
Copy the code

4.2.2 Immutable wrappers for arrays

For array ARR, plain wrappers are not enough, because we need to intercept not only method calls but also property access, such as arr[1] = true. JavaScript proxies enable us to do this:

const RE_INDEX_PROP_KEY = /^[0-9]+$/;
const ALLOWED_PROPERTIES = new Set([
  'length'.'constructor'.'slice'.'concat']);

function wrapArrayImmutably(arr) {
  const handler = {
    get(target, propKey, receiver) {
      // We assume that propKey is a string (not a symbol)
      if (RE_INDEX_PROP_KEY.test(propKey) // simplified check!
        || ALLOWED_PROPERTIES.has(propKey)) {
          return Reflect.get(target, propKey, receiver);
      }
      throw new TypeError(`Property "${propKey}"Can "t be accessed `); },set(target, propKey, value, receiver) {
      throw new TypeError('Setting is not allowed');
    },
    deleteProperty(target, propKey) {
      throw new TypeError('Deleting is not allowed'); }};return new Proxy(arr, handler);
}
Copy the code

Let’s wrap an array:

const arr = ['a'.'b'.'c'];
const wrapped = wrapArrayImmutably(arr);

// Non-destructive operations are allowed:
assert.deepEqual(
  wrapped.slice(1), ['b'.'c']);
assert.equal(
  wrapped[1], 'b');

// Destructive operations are not allowed:
assert.throws(
  () => wrapped[1] = 'x',
  /^TypeError: Setting is not allowed$/);
assert.throws(
  () => wrapped.shift(),
  /^TypeError: Property "shift"Can "t be accessed $/);Copy the code

4.3 How does immutability help share mutable state?

If the data is immutable, it can be shared without risk. In particular, there is no need to copy defensively.

Non-destructive updates complement immutable data and make it just as versatile as mutable data, but without the associated risks.

Avoid sharing mutable state libraries

JavaScript has several libraries available that support immutable data with non-destructive updates. Two popular ones are:

  • Immutable. Js provides Immutable (versioning) data structures such as lists, maps, Settings, and stacks.
  • Immer also supports immutable and non-destructive updates, but only ordinary objects and arrays. These libraries are described in more detail in the next two sections.

5.1 Immutable. Js

In its repository, Immutable is described as:

Immutable and persistent data collection for JavaScript, improved efficiency and simplicity.

Js provides immutable data structures such as:

  • The list of
  • Map(different from JavaScript’s built-in Map)
  • Set(different from JavaScript’s built-in Set)
  • The stack
  • The other.

In the following example, we use an immutable mapping:

import {Map} from 'immutable/dist/immutable.es.js';
const map0 = Map([
  [false.'no'],
  [true.'yes']]); const map1 = map0.set(true.'maybe'); // (A) assert.ok(map1 ! == map0); // (B) assert.equal(map1.equals(map0),false);

const map2 = map1.set(true.'yes'); // (C) assert.ok(map2 ! == map1); assert.ok(map2 ! == map0); assert.equal(map2.equals(map0),true); // (D)
Copy the code

Explanation:

  • In line A, we create A new, different version of map0, map1, where true is mapped to ‘maybe’.
  • In line B, we check to see if the change is non-destructive.
  • In line C, we update map1 and undo the changes we made in line A.
  • In line D, we use the immutable built-in.equals() method to check if we really undid the change

5.2 Immer

In its branches, the Immer library is described as:

Create the next immutable state by changing the current state.

Immer helps to update (possibly nested) ordinary objects and arrays without damaging them. That is, no special data structures are involved.

Using Immer looks like this:

import {produce} from 'immer/dist/immer.module.js';

const people = [
  {name: 'Jane', work: {employer: 'Acme'}},]; const modifiedPeople = produce(people, (draft) => { draft[0].work.employer ='Cyberdyne';
  draft.push({name: 'John', work: {employer: 'Spectre'}});
});

assert.deepEqual(modifiedPeople, [
  {name: 'Jane', work: {employer: 'Cyberdyne'}},
  {name: 'John', work: {employer: 'Spectre'}},]); assert.deepEqual(people, [ {name:'Jane', work: {employer: 'Acme'}},]);Copy the code

The raw data stored in people.produce() provides us with a variable draft. We assume that the variable is people and use operations that are typically used to make disruptive changes. Immer intercepted these operations. Rather than a mutant draft, it changes people in a nondestructive way. The results are quoted in the modified people. Generate modifiedPeople, which is immutable.

6 credit

Ron Korvig reminded me to use static factory methods instead of overloading constructors for deep copy of JavaScript.

7 Further Reading

  • Structural assignment (also called extension assignment) : JavaScript for thirsty code “Spreading into object literals”, “Spreading into Array literals exploringjs.com/impatient-j…”

  • Properties: the Speaking JavaScript “Property Attributes and Property Descriptors” “Protecting Objects” speakingjs.com/es5/ch17.ht…

  • Prototype chains: “JavaScript for thirsty” “Prototype chains” “Speaking JavaScript” Definition Versus the Assignment”

  • Speaking JavaScript “Metaprogramming with Proxies”