Javascript 共享可变状态的问题及规避方案

这篇博文回答了如下问题:html

  • 什么是共享可变状态?
  • 为何会有问题?
  • 如何避免它的问题? 标记为“(advance)”的部分更深刻,若是你想更快地阅读这篇博客文章,能够跳过它。

主要内容c++

  1. 什么是共享可变状态,为何会有问题?
  2. 避免经过复制数据来共享
    • 浅拷贝vs.深拷贝
    • JavaScript中的浅拷贝
    • JavaScript深度复制
    • 复制如何帮助共享可变状态?
  3. 经过非破坏性更新来避免突变
    • 背景:破坏性更新与非破坏性更新
    • 非破坏性更新如何帮助共享可变状态?
  4. 经过使数据不可变来防止突变
    • 背景:JavaScript的不变性
    • 不可改变的包装器(advance)
    • 不变性如何帮助共享可变状态?
  5. 避免共享可变状态的库
    • Immutable.js
    • Immer
  6. 鸣谢
  7. 进一步的阅读

1 什么是共享可变状态,为何会有问题?

共享可变状态工做以下:正则表达式

  • 若是两个或多个部分能够更改相同的数据(变量、对象等)
  • 若是他们的生命周期重叠, 而后,存在这种状况,一方的修改妨碍另外一方正确工做的风险。这是一个例子:
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:'
复制代码

这里有两个独立的部分:函数logElements()和函数main()。后者但愿在排序以前和以后记录一个数组。可是,它使用logElements()来清除参数。所以,main()在第A行记录一个空数组。json

在这篇文章的其他部分,咱们将讨论三种避免共享可变状态问题的方法:数组

  • 避免经过复制数据来共享安全

  • 经过非破坏性更新来避免突变bash

  • 经过使数据不可变来防止突变数据结构

接下来,咱们将回到咱们刚刚看到的例子并修正它。app

2 避免经过复制数据来共享

在讨论复制如何避免共享以前,咱们须要看看如何在JavaScript中复制数据。函数

2.1 浅拷贝vs.深拷贝

数据复制有两个“深度”:

  • 浅复制只复制对象和数组的顶级条目。条目值在原始和复制时仍然相同。
  • 深度复制也复制条目的值,不一样的是,它会从根节点遍历完整的树,并复制全部节点。

下一节将介绍这两种复制。不幸的是,JavaScript只内置了对浅拷贝的支持。若是咱们须要深度复制,咱们须要本身实现它。

2.2 JavaScript中的浅拷贝

让咱们来看看几种简单复制数据的方法。

2.2.1 经过扩展复制普通对象和数组

咱们能够扩展到对象文字和数组文字复制:

const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];
复制代码

不过,扩展复制有几个限制:

  • 原型没有被复制:
class MyClass {}

const original = new MyClass();
assert.equal(MyClass.prototype.isPrototypeOf(original), true);

const copy = {...original};
assert.equal(MyClass.prototype.isPrototypeOf(copy), false);
复制代码
  • 特殊的对象,如正则表达式和日期,具备特殊属性的“内部插槽”,不会被复制

  • 只复制本身的(非继承的)属性。考虑到原型链是如何工做的,这一般是最好的方法。但你仍然须要意识到这一点。在下面的示例中,original的继承属性. inheritedprop在copy中不可用,由于咱们只复制本身的属性,不保留原型。

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');
复制代码
  • 只复制可枚举的属性。例如,数组实例的自身属性.length不可枚举,也不可复制:
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);
复制代码
  • 独立于属性的属性,它的副本将始终是一个可写和可配置的数据属性-例如:
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,
  },
});
复制代码

这意味着getter和setter也不会被忠实地复制:属性值(用于数据属性)、get(用于getter)和set(用于setter)是独立的。

const original = {
  get myGetter() { return 123 },
  set mySetter(x) {},
};
assert.deepEqual({...original}, {
  myGetter: 123, // not a getter anymore!
  mySetter: undefined,
});
复制代码
  • 复制是浅层次的:复制具备原始数据中每一个键-值条目的新版本,可是原始数据的值自己不会被复制。例如:
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'}});
复制代码

有些限制能够消除,有些则不能:

  • 咱们能够在复制的过程当中给副本相同的原型:
class MyClass {}

const original = new MyClass();

const copy = {
  __proto__: Object.getPrototypeOf(original),
  ...original,
};
assert.equal(MyClass.prototype.isPrototypeOf(copy), true);
复制代码

或者,咱们能够在副本建立以后经过Object.setPrototypeOf()设置副本的原型。

  • 没有简单的方法来复制特殊对象。
  • 正如前面提到的,只复制本身的属性是一种特性而不是限制。
  • 咱们可使用Object.getOwnPropertyDescriptors()和Object.defineProperties()来复制对象(后面将解释如何这样作):
    • 它们考虑全部属性(不只仅是值),所以正确地复制getter、setter、只读属性等。
    • Object.getownpropertydescriptors()既检索可枚举的属性,也检索不可枚举的属性。
  • 咱们将在这篇文章的后面讨论深度复制。

2.2.2 经过Object.assign()进行浅复制(advance)

assign()的工做方式大多相似于将对象扩展到对象中。也就是说,如下两种复制方式基本相同:

const copy1 = {...original};
const copy2 = Object.assign({}, original);
复制代码

使用方法而不是语法的好处是,它能够经过库在旧的JavaScript引擎上填充。

不过,Object.assign()并不彻底像spread那样。它有一个比较微妙的不一样点:它以不一样的方式建立属性。

  • assign()使用assign建立副本的属性。
  • 扩展定义了拷贝中的新属性。

在其余方面,赋值(assign)调用本身的和继承的setter,而定义(这里指的扩展)不调用(关于赋值与定义的更多信息)。这种差别不多被注意到。下面的代码是一个例子,但它是人为的:

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);
复制代码

2.2.3 经过Object.getOwnPropertyDescriptors()和Object.defineProperties()(advance)进行浅复制

JavaScript容许咱们经过属性描述符建立属性,即指定属性属性的对象。例如,经过Object.defineProperties(),咱们已经在实际中看到了它。若是咱们把这个方法和Object.getOwnPropertyDescriptors()结合起来,咱们能够更忠实地复制:

function copyAllOwnProperties(original) {
  return Object.defineProperties(
    {}, Object.getOwnPropertyDescriptors(original));
}
复制代码

这消除了经过扩展复制对象的两个限制。

首先,正确复制本身属性的全部属性。所以,咱们如今能够复制本身的getter和setter:

const original = {
  get myGetter() { return 123 },
  set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);
复制代码

其次,因为Object.getOwnPropertyDescriptors(),不可枚举的属性也被复制了:

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);
复制代码

2.3 JavaScript深拷贝

如今是时候解决深层复制了。首先,咱们将手动深拷贝,而后咱们将检查通用方法。

2.3.1 经过嵌套扩展手动深度复制

若是咱们嵌套扩展,咱们获得深层拷贝:

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);
复制代码

HACK:经过JSON 进行通用深度复制 这是一个骇客方法,但在紧要关头,它提供了一个快速解决方案:为了深度复制一个对象的原始,咱们首先把它转换成一个JSON字符串和解析那个JSON字符串:

function jsonDeepCopy(original) {
  return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
复制代码

这种方法的显著缺点是,咱们只能复制JSON支持的键和值的属性。

一些不支持的键和值被简单地忽略:

assert.deepEqual(
  jsonDeepCopy({
    [Symbol('a')]: 'abc',
    b: function () {},
    c: undefined,
  }),
  {} // empty object
);
复制代码

其余缘由例外:

assert.throws(
  () => jsonDeepCopy({a: 123n}),
  /^TypeError: Do not know how to serialize a BigInt$/);
复制代码

2.3.3 实现通用深度复制

下面的函数通常深拷贝一个原始的值:

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
    return original;
  }
}
复制代码

该函数处理三种状况:

  • 若是original是一个数组,咱们建立一个新的数组,并深拷贝原始的元素到它。
  • 若是original是一个对象,咱们使用相似的方法。
  • 若是original是一个原始值,咱们不须要作任何事情。

让咱们试试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);
复制代码

请注意,deepCopy()只修复了一个扩展问题:浅复制。全部其余的都保持不变:原型不被复制,特殊对象只被部分复制,不可枚举的属性被忽略,大多数属性属性被忽略。

实现彻底的复制一般是不可能的:不是全部的数据都是树,有时你不想要全部的属性,等等。

deepCopy()的更简洁版本

若是咱们使用.map()和Object.fromEntries(),咱们可使以前的deepCopy()实现更简洁:

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
    return original;
  }
}
复制代码

2.3.4 在类中实现深度复制(advance)

一般使用两种技术来实现类实例的深度复制:

  • .clone()方法
  • 复制构造函数

.clone() methods

该技术为每一个要深度复制其实例的类引入了一个.clone()方法。它会返回一个深度副本。下面的示例显示了能够克隆的三个类。

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)
  }
}
复制代码

第A行演示了此技术的一个重要方面:还必须递归地克隆复合实例属性值。

静态工厂方法

复制构造函数是使用当前类的另外一个实例来设置当前实例的构造函数。复制构造函数在静态语言(如c++和Java)中很流行,在这些语言中,能够经过静态重载(静态意味着在编译时发生)提供构造函数的多个版本。

在JavaScript中,你能够这样作(但不是很优雅):

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;
    }
  }
}
复制代码

你能够这样使用这个类:

const original = new Point(-1, 4);
const copy = new Point(original);
assert.deepEqual(copy, original);
复制代码

相反,静态工厂方法在JavaScript中工做得更好(静态意味着它们是类方法)。

在下面的例子中,三个类Point, Color和ColorPoint都有一个静态的工厂方法.from():

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)
  }
}
复制代码

在第A行,咱们再次使用递归复制。

这就是ColorPoint.from()的工做方式:

const original = new ColorPoint(-1, 4, new Color('red'));
const copy = ColorPoint.from(original);
assert.deepEqual(copy, original);
复制代码

2.4 复制如何帮助共享可变状态?

只要咱们只从共享状态读取,就不会有任何问题。在咱们修改它以前,咱们须要“取消共享”它,经过复制它(尽量深刻)。

防护性复制是一种在可能出现问题时进行复制的技术。它的目标是保持当前实体(函数、类等)的安全

  • 输入:复制(可能的)传递给咱们的共享数据,让咱们在不受外部实体干扰的状况下使用这些数据。
  • 输出:在将内部数据暴露给外部方以前复制它,意味着该方不能破坏咱们的内部活动。

请注意,这些措施保护咱们不受其余方的侵害,但它们也保护其余方不受咱们的侵害。

下一节将演示这两种防护性复制。

2.4.1 复制共享输入

请记住,在本文开头的激励示例中,咱们遇到了麻烦,由于logElements()修改了它的参数arr:

function logElements(arr) {
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}
复制代码

让咱们添加防护复制到这个函数:

function logElements(arr) {
  arr = [...arr]; // defensive copy
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}
复制代码

如今logElements()再也不引发问题,若是它是调用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'

复制代码

2.4.2 复制公开的内部数据

让咱们从一个不复制其公开的内部数据的类StringBuilder开始(第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('');
  }
}
复制代码

只要没有使用.getParts(),一切均可以正常工做:

const sb1 = new StringBuilder();
sb1.add('Hello');
sb1.add(' world!');
assert.equal(sb1.toString(), 'Hello world!');
复制代码

可是,若是.getParts()的结果改变(行A),则StringBuilder将中止正常工做:

const sb2 = new StringBuilder();
sb2.add('Hello');
sb2.add(' world!');
sb2.getParts().length = 0; // (A)
assert.equal(sb2.toString(), ''); // not OK
复制代码

解决方案是在暴露以前防护性地复制内部的._data(行A):

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

如今,更改.getParts()的结果再也不干扰sb的操做:

const sb = new StringBuilder();
sb.add('Hello');
sb.add(' world!');
sb.getParts().length = 0;
assert.equal(sb.toString(), 'Hello world!'); // OK
复制代码

3 经过非破坏性地更新来避免突变

咱们将首先探讨破坏性数据更新和非破坏性数据更新的区别。而后咱们将学习非破坏性更新如何避免突变。

3.1 背景:破坏性更新与非破坏性更新

咱们能够区分两种不一样的数据更新方式:

  • 数据的破坏性更新会使数据发生变化,从而产生所需的表单。
  • 数据的非破坏性更新将建立具备所需表单的数据的副本。 后一种方法相似于首先复制一个副本,而后破坏性地更改它,但这两种方法同时进行。

3.1.1 示例:破坏性地和非破坏性地更新对象

这是咱们如何破坏性地设置一个对象的属性.city:

const obj = {city: 'Berlin', country: 'Germany'};
const key = 'city';
obj[key] = 'Munich';
assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
复制代码

如下函数非破坏性地改变属性:

function setObjectNonDestructively(obj, key, value) {
  const updatedObj = {};
  for (const [k, v] of Object.entries(obj)) {
    updatedObj[k] = (k === key ? value : v);
  }
  return updatedObj;
}
复制代码

它的用法以下:

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'});
复制代码

扩展使setobjectnondestrucative()更简洁:

function setObjectNonDestructively(obj, key, value) {
  return {...obj, [key]: value};
}
复制代码

注意:两个版本的setobjectnondestrucative()更新都很浅。

3.1.2 示例:破坏性地和非破坏性地更新数组

这是咱们如何破坏性地设置一个数组的元素:

const original = ['a', 'b', 'c', 'd', 'e'];
original[2] = 'x';
assert.deepEqual(original, ['a', 'b', 'x', 'd', 'e']);
复制代码

非破坏性更新数组要比非破坏性更新对象复杂得多。

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']);
复制代码

.slice()和spread使setarraynondestructive()更简洁:

function setArrayNonDestructively(arr, index, value) {
  return [
  ...arr.slice(0, index), value, ...arr.slice(index+1)]
}
复制代码

注意:setarraynondestrucsive()的两个版本更新都很浅。

3.1.3 手动深更新

到目前为止,咱们只是粗略地更新了数据。让咱们来解决深层更新。下面的代码演示了如何手动执行此操做。咱们正在更改姓名和雇主。

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'}});
复制代码

3.1.4 实现通用深度更新

下面的函数实现了通用的深度更新。

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
    return original;
  }
}
复制代码

若是咱们将值视为正在更新的树的根,那么deepUpdate()只会对单个分支(第A行和第C行)进行深度更改,而对其余全部分支的复制都比较浅(第B行和第D行)。

这就是使用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'}});
复制代码

3.2 非破坏性更新如何帮助共享可变状态?

使用非破坏性更新,共享数据就变得不成问题,由于咱们从不改变共享数据。(显然,这只有在各方都这么作的状况下才有效。)

有趣的是,复制数据变得很是简单:

const original = {city: 'Berlin', country: 'Germany'};
const copy = original;
复制代码

只有在必要的状况下,而且咱们正在进行非破坏性的更改时,才会实际复制原件。

4 经过使数据不可变来防止突变

咱们能够经过使数据不可变来防止共享数据的突变。接下来,咱们将研究JavaScript如何支持不变性。而后,咱们将讨论不可变数据如何帮助共享可变状态。

4.1 背景:JavaScript的不变性

JavaScript有三层保护对象:

  • 防止扩展使得向对象添加新属性变得不可能。不过,您仍然能够删除和更改属性。
    • 方法:Object.preventExtensions (obj)
  • 密封能够防止扩展,并使全部属性不可配置(大体:您不能再更改属性的工做方式)。
    • 方法:Object.seal (obj)
  • 冻结一个对象后,使其全部属性不可写。也就是说,对象是不可扩展的,全部属性都是只读的,没有办法改变。
    • 方法:Object.freeze (obj)

鉴于咱们但愿咱们的对象是彻底不可变的,咱们在这篇博客文章中只使用Object.freeze()。

4.1.1 冻结很浅

Object.freeze(obj)只冻结obj及其属性。它不会冻结这些属性的值-例如:

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'],
  });
复制代码

4.1.2 实现深冻结

若是咱们想要深度冻结,咱们须要本身来实施:

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;
}
复制代码

从新访问上一节的例子,咱们能够检查deepFreeze()是否真的冻结得很深:

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$/);
复制代码

4.2 不可改变的包装器(advance)

不可变包装器包装可变集合并提供相同的API,但没有破坏性操做。如今,对于同一个集合,咱们有两个接口:一个是可变的,另外一个是不可变的。当咱们须要安全地公开可变的内部数据时,这是很是有用的。

接下来的两个部分将展现映射和数组的包装器。二者都有如下局限性:

  • 他们是草图。须要作更多的工做来使它们适合于实际应用:更好的检查,支持更多的方法,等等。
  • 他们浅浅地工做。

4.2.1 映射的不可变包装器

类 ImmutableMapWrapper 为 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);
  }
}
复制代码

示例以下:

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$/);
复制代码

4.2.2 数组的不可变包装器

对于数组arr,普通的包装是不够的,由于咱们不只须要拦截方法调用,还须要拦截属性访问,好比arr[1] = true。JavaScript代理使咱们可以作到这一点:

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);
}
复制代码

让咱们包装一个数组:

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$/);
复制代码

4.3 不变性如何帮助共享可变状态?

若是数据是不可变的,那么能够毫无风险地共享它。特别是,没有必要采起防护性的复制。

非破坏性的更新补充了不可变数据,并使其与可变数据同样多用途,但没有相关的风险。

5 避免共享可变状态的库

JavaScript有几个库可使用,它们支持具备非破坏性更新的不可变数据。两个流行的是:

  • Immutable.js提供了不可变的(版本)数据结构,如列表、映射、设置和堆栈。
  • Immer还支持不变性和非破坏性更新,但只支持普通对象和数组。 这些库将在接下来的两个小节中进行更详细的描述。

5.1 Immutable.js

在其存储库中,Immutable.js被描述为:

用于JavaScript的不可变的持久数据收集,提升了效率和简单性。

js提供了不可变的数据结构,如:

  • 列表
  • Map(与JavaScript的内置Map不一样)
  • Set(与JavaScript的内置Set不一样)
  • 堆栈
  • 其余。

在下面的例子中,咱们使用一个不可变的映射:

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)
复制代码

解释:

  • 在第A行,咱们建立一个新的,不一样的map0版本map1,其中true被映射到'maybe'。
  • 在B行中,咱们检查更改是不是非破坏性的。
  • 在第C行中,咱们更新map1并撤消在第A行中所作的更改。
  • 在第D行,咱们使用了不可变的内置.equals()方法来检查咱们是否真的取消了更改

5.2 Immer

在其分支中,Immer库被描述为:

经过改变当前状态来建立下一个不可变状态。

Immer有助于无破坏性地更新(多是嵌套的)普通对象和数组。也就是说,不涉及任何特殊的数据结构。

使用Immer是这样的:

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'}},
]);
复制代码

原始数据存储在people.produce()为咱们提供了一个可变的草稿。咱们假设这个变量是people,并使用一般用于进行破坏性更改的操做。Immer拦截了这些行动。而不是突变草稿,它无损地改变people。结果被修改过的people引用。生成modifiedPeople,它是不可改变的。

6 鸣谢

Ron Korvig提醒我使用静态工厂方法,而不是重载构造函数来进行JavaScript的深度复制。

7 进一步阅读

  • 结构赋值(也是说扩展赋值): 《JavaScript for impatient programmers》 “Spreading into object literals” ,“Spreading into Array literals” exploringjs.com/impatient-j…

  • 属性: 《Speaking JavaScript》“Property Attributes and Property Descriptors” “Protecting Objects” speakingjs.com/es5/ch17.ht…

  • 原型链: 《JavaScript for impatient programmers》“Prototype chains” 《Speaking JavaScript》 “Properties: Definition Versus Assignment”

  • 《Speaking JavaScript》“Metaprogramming with proxies”

相关文章
相关标签/搜索