談一談共享可變數據存在的問題以及如何避免它

本文是對shared-mutable-state這篇文章的一個解讀分析,帶你從頭理解下共享可變數據的前世此生,這篇文章主要闡述瞭如下3個問題:javascript

  • 什麼是共享可變數據?
  • 它爲何存在問題?
  • 怎樣避免這個問題?

1 什麼是共享可變數據以及它存在的問題

共享可變數據就是超過2個以上的實例可以改變同一個數據,好比以下例子:html

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:'
複製代碼

這個例子,main()logElements()這兩個函數都引用了數組arr,其中logElements方法體內經過調用shift方法改變了arr數組,致使了後面一個logElements方法輸出了空數組。java

2 經過拷貝來避免共享數據存在的問題

文中說起了能夠經過拷貝數據來解決這個問題,其中拷貝又分爲淺拷貝和深拷貝:git

  • 淺拷貝只會拷貝數組或者對象第一層級的實體(屬性),實體的值依然和原始數據的值一摸同樣,若是實體的值是引用類型(數組或者對象),那若是共享實體的值依然會存在上述問題。
  • 深拷貝不只會拷貝數組或者對象的第一層級的實體,同時會拷貝實體的值,若是實體的值是數組或者對象,也會一層一層的深刻進行拷貝,拷貝出來的值和原始值就是物理意義上隔絕的兩個值。

不論是淺拷貝仍是深拷貝,都有其使用的場景,好比若是一個對象或者數組層級很簡單,值都是基本數據類型,那使用淺拷貝便可,相比深拷貝,代碼執行效率更高且佔用更少的內存。github

文中同時還提到了不少實現拷貝的方式,我下面來一一介紹下,其中不少你可能聽過但你不必定了解的很透徹。正則表達式

2.1 ES6擴展符

首先說起的是經過...擴展符的方式,拷貝實現方式以下:json

const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];
複製代碼

然而經過擴展符實現拷貝存在幾個侷限性:數組

  1. 沒法拷貝原始值的原型(__proto__屬性指向的值)
  2. 特殊對象(如正則表達式和日期)具備特殊的內部插槽,不會被複制。
  3. 只有本身的屬性,不包括經過繼承而來的屬性可以被複制
  4. 只有enumerable(可枚舉)屬性可以被複制
  5. 一些特殊屬性,好比writable``configurable
  6. 這種方式是淺拷貝

面對這些問題,文中也提供了一些解決的方式,感興趣的能夠查看原文數據結構

2.2 Object.assign()

接着,又介紹了經過對象的assign()方法的方式:app

const copy1 = {...original};
const copy2 = Object.assign({}, original);
複製代碼

Object.assign()的使用方式以及侷限性和擴展符差很少,但也有點區別:

  • Object.assign()經過重新賦值來修改原始對象來建立拷貝對象
  • ...擴展符經過使用現有對象的自身屬性來建立新的普通對象

2.3 Object.getOwnPropertyDescriptors()和Object.defineProperties()

文中還列舉了一些解決淺拷貝缺陷的一些解決方式,衆所周知,淺拷貝本質上是經過Object.defineProperties()這個方法,直接在一個對象上定義新的屬性或修改現有屬性,並返回該對象來實現的,結合Object.getOwnPropertyDescriptors()方法,咱們可以實現一種拷貝方式,能夠輕鬆解決擴展符拷貝存在的侷限性。

function copyAllOwnProperties(original) {
  return Object.defineProperties(
    {}, Object.getOwnPropertyDescriptors(original));
}
複製代碼

經過上述方式,咱們如今不只可以拷貝本身的屬性,同時非枚舉一樣可以被拷貝。

3 深拷貝

而後,文中接着介紹了幾種深拷貝的方式

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);
複製代碼

3.2 經過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);
複製代碼

同時,Symbol和空值拷貝的時候都會被忽略

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;
  }
}
複製代碼

一個更加簡潔的方式,若是拷貝的是對象,先經過Object.entries獲取全部自身可枚舉屬性的鍵值對數組,遍歷鍵值對數組,而後經過Object.fromEntries還原成對象。

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;
  }
}
複製代碼

3.4 深拷貝classes類

還介紹了深拷貝class類的方式

  • .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)
  }
}
複製代碼

這裏須要注意的是,組合實例屬性須要遞歸拷貝。

4 拷貝如何幫助共享可變狀態

前面花了那麼多時間來介紹拷貝的方式,那拷貝是如何幫助咱們共享可變狀態的呢?其實只要控制好兩個方面就行,一個是進入,一個是輸出。

假如,有一個共享數據,在訪問這個數據前(進入),咱們能夠經過合適的拷貝這份數據,那無論咱們怎麼操做拷貝後的數據,都不會影響原始數據。另外一個就是輸出,假如咱們將輸入暴露出去供別人使用,咱們不要直接暴露原始數據,能夠暴露一份拷貝數據,這樣無論他人如何操控這份暴露出去的數據,都不會影響咱們的原始數據。

拿最開始的例子來說:

初始是這樣

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());
  }
}
複製代碼

如今再去執行後面的操做就不會發生數據爲空的狀況

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'
複製代碼

同理,輸出拷貝的數據也是同樣的緣由。

5 非破壞性更新數據

數據的共享不只是獲取,有時候咱們還會更新數據,那原則就是必須非破壞性的更新,不要破壞性的更新。

非破壞性就是指不要直接去操做原始數據,強行變化原始的數據結構,儘可能經過拷貝的方式,對原始數據侵入性最弱的方式去更新數據。

看以下例子:

直接賦值改變原始值的方式就輸入破壞性的操做方式

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');
複製代碼

文中還說起了非破壞性的更新數組,經過深拷貝非破壞性的更新數據的方式,感興趣的能夠查看原文

爲何要經過非破壞性的方式更新數據呢?

由於經過非破壞性更新,共享數據就不會由於破壞性更新數據致使數據先後不一致的問題,同時也利於數據回溯。

既然無論怎樣都不直接操縱原始數據,這裏就引伸出瞭如今愈來愈流行的一個概念,對原始數據的一個新稱呼 不可變數據

6 不可變數據

那如何使數據不可變呢?javascript提供了3種方式:

  1. Object.preventExtensions(obj) 讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性
  2. Object.seal(obj) 封閉一個對象,阻止添加新屬性並將全部現有屬性標記爲不可配置。當前屬性的值只要可寫就能夠改變
  3. Object.freeze(obj) 凍結一個對象,一個被凍結的對象不再能被修改,但須要注意的是只會凍結本身和本身的屬性,不會凍結屬性的值

那如何實現深度凍結?

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;
}
複製代碼

7 Immutable.js和Immer

文中最後說起了兩個提供了建立不可變數據以及非暴力更新數據的能力的庫

  • Immutable.js提供了List Map Set Stack的不可變數據結構
  • Immer一樣提供了相似的能力

8 總結

經過閱讀全文,咱們知道了在共享同一份數據,爲什麼要保持數據不可變,這也是爲何使用Redux的進行狀態管理的時候,不容許咱們直接改變數據,以及咱們通常會配套使用Immutable.js的真正緣由。Redux只有一份所有的狀態,那麼多組件引用它,若是不保持數據的純潔性,數據管理就會變得異常困難,遇到問題也會難以追溯。

最後,但願這篇文章可以提高你對數據管理的深度認知以及擴展管理數據的一些方式。

相關文章
相關標籤/搜索