JavaScript: Immutable Object

20th August 2020 at 2:19pm
JavaScript

不可变对象,即一个对象在创建之后它的内容不再被修改。在函数式编程的场景中(比如 Redux)经常被使用。如果需要修改数据,不应该修改原本的对象,而应该拷贝一个新的对象做修改,再替换原来的变量指向的对象。比如下面演示一段修改字段值的代码:

let obj = { a: 1, b: 2 };
let modifiedObj = { ...obj, a: 3};

JS 实现不可变对象有语言内置的方式,也有第三方库。

语言内置的方式

Spread operatorObject.assign 可以实现。一般用 spread operator,因为更简单好理解。Object.assign 的方式不在这里描述。

对于数组:

const numbers = [1, 2, 3];

// Adding
const index = numbers.indexOf(2);
const added = [...numbers.slice(0, index), 4, ...numbers.slice(index)];

// Removing
const removed = numbers.filter(n => n !== 2);

// Updating
const updated = numbers.map(n => (n === 2 ? 20 : n));
console.log(updated);

对于 Object

const address = {
  country: "USA",
  city: "San Francisco"
};

// Add new field
const added = {
  ...address,
  zipCode: 94102
};

// Removing (country field is removed)
const { country, ...removed } = address;
console.log(removed);

// Updating
const updated = {
  ...address,
  city: "New York"
};
console.log(person);

注意:spread operator 使用的是 浅复制。这意味着你在复制多层 object 的结构时需要额外注意。使用第三方库会自动帮你解决这些问题。

第三方库

Immer

ImmerMosh 最推荐的库。

它的原理是,提供一个代理了原始数据 baseState 的草稿对象 draftState,你在这个草稿上做的改动会被 Immer 纪录,用来生成一个新的对象:

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

Redux Toolkit 底层使用了 Immer。

Immutable.js

Immutable.js 是 Facebook 维护的。它自定义了一个 Map 对象,在这个 Map 上做 set() 操作修改字段:

import { Map } from "immutable";

let book = Map({ title: "Harry Potter" });

function publish(book) {
  return book.set("isPublished", true);
}

book = publish(book);

console.log(book.toJS());

Immer v.s. Immutable.js

Immer 在实现上更优雅,你操作的是就是一个 JavaScript object,而 Immutable.js 需要使用它自定义的 Map 结构。这使得后者在与第三方库整合上势必会更麻烦。

应该使用 Immer。