深入理解 JavaScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

第七章 破坏性和非破坏性地更新数据



在本章中,我们将学习两种不同的数据更新方式

后一种方式类似于先创建副本,然后破坏性地更改副本,但它同时执行了这两项操作。

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