深入探討 JavaScript
請支持這本書:購買捐款
(廣告,請不要封鎖。)

7 以破壞性與非破壞性更新資料



在本章中,我們將學習兩種不同的資料更新方式

後者的方式類似於先建立副本,然後以破壞性更新副本,但它會同時執行這兩個動作。

7.1 範例:以破壞性與非破壞性更新物件

以下程式碼展示一個以破壞性更新物件屬性的函式,並在一個物件上使用它。

function setPropertyDestructively(obj, key, value) {
  obj[key] = value;
  return obj;
}

const obj = {city: 'Berlin', country: 'Germany'};
setPropertyDestructively(obj, 'city', 'Munich');
assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});

以下程式碼展示非破壞性更新物件

function setPropertyNonDestructively(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 = setPropertyNonDestructively(obj, 'city', 'Munich');

// We have created an updated object:
assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'});

// But we didn’t change the original:
assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});

展開運算式讓 setPropertyNonDestructively() 變得更簡潔

function setPropertyNonDestructively(obj, key, value) {
  return {...obj, [key]: value};
}

setPropertyNonDestructively() 的兩個版本都進行淺層更新:它們只變更物件的最上層。

7.2 範例:以破壞性與非破壞性更新陣列

以下程式碼展示一個以破壞性更新陣列元素的函式,並在一個陣列上使用它。

function setElementDestructively(arr, index, value) {
  arr[index] = value;
}

const arr = ['a', 'b', 'c', 'd', 'e'];
setElementDestructively(arr, 2, 'x');
assert.deepEqual(arr, ['a', 'b', 'x', 'd', 'e']);

以下程式碼展示非破壞性更新陣列

function setElementNonDestructively(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 = setElementNonDestructively(arr, 2, 'x');
assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']);
assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);

.slice() 和展開運算式讓 setElementNonDestructively() 變得更簡潔

function setElementNonDestructively(arr, index, value) {
  return [
    ...arr.slice(0, index), value, ...arr.slice(index+1)];
}

setElementNonDestructively() 的兩個版本都進行淺層更新:它們只變更陣列的最上層。

7.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'}});

7.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;
  }
}

如果我們將 value 視為我們正在更新的樹狀結構的根,則 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'}});