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」

相關文章
相關標籤/搜索