Skip to content

不可变性:构建可预测的数据流

为什么需要不可变性

在软件世界中, 数据的意外改变就像是房间里突然移动的家具——你以为桌子在这里, 但当你走过去时它已经在那里了。这种不确定性会导致难以追踪的 bug 和复杂的调试过程。

不可变性(Immutability)是函数式编程的核心原则之一。它的核心思想很简单:一旦创建了数据, 就不能再修改它。如果需要"改变"数据, 实际上是创建一个包含新值的全新数据副本。

这听起来可能会浪费内存, 但实际上带来的好处远超过成本:数据流变得可预测、代码更容易理解、并发问题大大减少、时间旅行调试成为可能。

可变性带来的问题

让我们先看看可变数据会导致什么问题。

1. 意外的副作用

javascript
// 一个看似无害的函数
function applyDiscount(product, discountRate) {
  product.price = product.price * (1 - discountRate);
  product.onSale = true;
  return product;
}

const laptop = {
  name: "MacBook Pro",
  price: 2000,
  onSale: false,
};

// 计算折扣价
const discountedLaptop = applyDiscount(laptop, 0.1);

console.log("原价:", laptop.price); // 1800 (被意外修改!)
console.log("促销价:", discountedLaptop.price); // 1800

// 原始对象被污染了
console.log("原对象状态:", laptop.onSale); // true (意外改变!)

这个例子中, 我们只是想计算折扣价, 但却意外地修改了原始产品对象。如果其他代码依赖原始价格, 就会出现 bug。

2. 难以追踪的状态变化

javascript
// 购物车管理
let shoppingCart = {
  items: [{ id: 1, name: "Laptop", price: 1000, quantity: 1 }],
  total: 1000,
};

function addItem(item) {
  shoppingCart.items.push(item);
  shoppingCart.total += item.price * item.quantity;
}

function removeItem(itemId) {
  const index = shoppingCart.items.findIndex((item) => item.id === itemId);
  if (index > -1) {
    const removed = shoppingCart.items.splice(index, 1)[0];
    shoppingCart.total -= removed.price * removed.quantity;
  }
}

// 在不同地方调用这些函数
addItem({ id: 2, name: "Mouse", price: 50, quantity: 2 });
// ... 100 行代码 ...
removeItem(1);
// ... 200 行代码 ...
addItem({ id: 3, name: "Keyboard", price: 80, quantity: 1 });

// 现在 shoppingCart 的状态是什么? 很难追踪!
// 如果 total 计算错误, 在哪里引入的 bug?

当多个函数都修改同一个对象时, 很难追踪状态在何时何地被改变。调试时你必须检查每一处可能的修改点。

3. 并发问题

javascript
// 多个操作同时修改数据
let balance = 1000;

async function withdraw(amount) {
  // 模拟异步操作
  await new Promise((resolve) => setTimeout(resolve, 100));

  if (balance >= amount) {
    balance -= amount;
    return { success: true, newBalance: balance };
  }
  return { success: false, message: "余额不足" };
}

// 同时执行两次取款
Promise.all([withdraw(600), withdraw(600)]).then((results) => {
  console.log("操作1:", results[0]);
  console.log("操作2:", results[1]);
  console.log("最终余额:", balance); // 可能是 -200! (竞态条件)
});

当多个操作同时修改共享数据时, 会产生竞态条件, 导致不可预测的结果。

不可变性的实践

1. 使用扩展运算符和数组方法

javascript
// ❌ 可变方式
const numbers = [1, 2, 3];
numbers.push(4); // 修改原数组
numbers[0] = 10; // 修改原数组

// ✅ 不可变方式
const originalNumbers = [1, 2, 3];
const withNewNumber = [...originalNumbers, 4];
const withModifiedFirst = [10, ...originalNumbers.slice(1)];

console.log(originalNumbers); // [1, 2, 3] (未被修改)
console.log(withNewNumber); // [1, 2, 3, 4]
console.log(withModifiedFirst); // [10, 2, 3]

常用的不可变数组操作:

javascript
const fruits = ["apple", "banana", "cherry"];

// 添加元素
const withOrange = [...fruits, "orange"];
const withGrapeAtStart = ["grape", ...fruits];

// 删除元素
const withoutBanana = fruits.filter((fruit) => fruit !== "banana");
const withoutFirst = fruits.slice(1);
const withoutLast = fruits.slice(0, -1);

// 修改元素
const capitalized = fruits.map((fruit) => fruit.toUpperCase());
const replacedCherry = fruits.map((fruit) =>
  fruit === "cherry" ? "mango" : fruit
);

// 排序 (sort 会修改原数组, 需要先复制)
const sorted = [...fruits].sort();

console.log("原数组:", fruits); // 始终是 ['apple', 'banana', 'cherry']

2. 对象的不可变操作

javascript
// ❌ 可变方式
const user = { name: "John", age: 25 };
user.age = 26; // 修改原对象
user.email = "[email protected]"; // 添加属性

// ✅ 不可变方式
const originalUser = { name: "John", age: 25 };
const olderUser = { ...originalUser, age: 26 };
const userWithEmail = { ...originalUser, email: "[email protected]" };

console.log(originalUser); // { name: "John", age: 25 } (未被修改)
console.log(olderUser); // { name: "John", age: 26 }
console.log(userWithEmail); // { name: "John", age: 25, email: "..." }

复杂对象的不可变更新:

javascript
const user = {
  id: 1,
  name: "Sarah",
  address: {
    city: "New York",
    street: "5th Avenue",
    zipCode: "10001",
  },
  preferences: {
    theme: "dark",
    language: "en",
  },
};

// 更新嵌套属性
const withNewCity = {
  ...user,
  address: {
    ...user.address,
    city: "San Francisco",
  },
};

// 更新多个嵌套属性
const updated = {
  ...user,
  name: "Sarah Johnson",
  address: {
    ...user.address,
    city: "Los Angeles",
    zipCode: "90001",
  },
  preferences: {
    ...user.preferences,
    theme: "light",
  },
};

console.log("原对象:", user.address.city); // "New York" (未改变)
console.log("新对象:", withNewCity.address.city); // "San Francisco"

3. 构建不可变的工具函数

javascript
// 通用的不可变更新函数
const updateObject = (obj, updates) => ({ ...obj, ...updates });

const updateNested = (obj, path, value) => {
  const keys = path.split(".");
  const lastKey = keys.pop();

  let result = { ...obj };
  let current = result;

  for (const key of keys) {
    current[key] = { ...current[key] };
    current = current[key];
  }

  current[lastKey] = value;
  return result;
};

// 使用
const user = {
  profile: {
    personal: {
      name: "John",
      age: 25,
    },
    contact: {
      email: "[email protected]",
    },
  },
};

const updated = updateNested(user, "profile.personal.age", 26);

console.log(user.profile.personal.age); // 25 (未改变)
console.log(updated.profile.personal.age); // 26

通用的数组操作工具:

javascript
// 不可变的数组工具
const arrayUtils = {
  // 在指定位置插入
  insertAt: (arr, index, item) => [
    ...arr.slice(0, index),
    item,
    ...arr.slice(index),
  ],

  // 删除指定位置
  removeAt: (arr, index) => [...arr.slice(0, index), ...arr.slice(index + 1)],

  // 更新指定位置
  updateAt: (arr, index, updater) =>
    arr.map((item, i) => (i === index ? updater(item) : item)),

  // 移动元素
  move: (arr, fromIndex, toIndex) => {
    const item = arr[fromIndex];
    const without = arrayUtils.removeAt(arr, fromIndex);
    return arrayUtils.insertAt(without, toIndex, item);
  },
};

const numbers = [1, 2, 3, 4, 5];

console.log(arrayUtils.insertAt(numbers, 2, 99)); // [1, 2, 99, 3, 4, 5]
console.log(arrayUtils.removeAt(numbers, 2)); // [1, 2, 4, 5]
console.log(arrayUtils.updateAt(numbers, 2, (n) => n * 10)); // [1, 2, 30, 4, 5]
console.log(arrayUtils.move(numbers, 0, 4)); // [2, 3, 4, 5, 1]

console.log(numbers); // [1, 2, 3, 4, 5] (始终未改变)

实际应用场景

1. React 状态管理

在 React 中, 不可变性是正确更新状态的关键:

javascript
// React 组件中的状态更新
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "学习 JavaScript", completed: false },
    { id: 2, text: "写代码", completed: false },
  ]);

  // ❌ 错误的方式 - 直接修改状态
  const toggleWrong = (id) => {
    const todo = todos.find((t) => t.id === id);
    todo.completed = !todo.completed; // 修改了原对象
    setTodos(todos); // React 无法检测到变化!
  };

  // ✅ 正确的方式 - 创建新数组
  const toggleCorrect = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // 添加新待办
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, completed: false }]);
  };

  // 删除待办
  const removeTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  // 清除已完成
  const clearCompleted = () => {
    setTodos(todos.filter((todo) => !todo.completed));
  };
}

2. Redux 风格的状态管理

javascript
// 不可变的 reducer
const initialState = {
  users: [],
  loading: false,
  error: null,
};

function usersReducer(state = initialState, action) {
  switch (action.type) {
    case "FETCH_USERS_START":
      return {
        ...state,
        loading: true,
        error: null,
      };

    case "FETCH_USERS_SUCCESS":
      return {
        ...state,
        loading: false,
        users: action.payload,
      };

    case "ADD_USER":
      return {
        ...state,
        users: [...state.users, action.payload],
      };

    case "UPDATE_USER":
      return {
        ...state,
        users: state.users.map((user) =>
          user.id === action.payload.id
            ? { ...user, ...action.payload.updates }
            : user
        ),
      };

    case "DELETE_USER":
      return {
        ...state,
        users: state.users.filter((user) => user.id !== action.payload),
      };

    default:
      return state;
  }
}

// 使用示例
let state = initialState;

state = usersReducer(state, {
  type: "ADD_USER",
  payload: { id: 1, name: "John", email: "[email protected]" },
});

state = usersReducer(state, {
  type: "ADD_USER",
  payload: { id: 2, name: "Jane", email: "[email protected]" },
});

state = usersReducer(state, {
  type: "UPDATE_USER",
  payload: { id: 1, updates: { email: "[email protected]" } },
});

console.log(state.users);

3. 历史记录和撤销功能

不可变性使得实现撤销/重做功能变得简单:

javascript
class HistoryManager {
  constructor(initialState) {
    this.history = [initialState];
    this.currentIndex = 0;
  }

  // 执行新操作
  execute(updater) {
    // 获取当前状态
    const currentState = this.getCurrentState();

    // 创建新状态
    const newState = updater(currentState);

    // 删除当前位置之后的历史
    this.history = this.history.slice(0, this.currentIndex + 1);

    // 添加新状态
    this.history.push(newState);
    this.currentIndex++;

    return newState;
  }

  // 撤销
  undo() {
    if (this.canUndo()) {
      this.currentIndex--;
      return this.getCurrentState();
    }
    return null;
  }

  // 重做
  redo() {
    if (this.canRedo()) {
      this.currentIndex++;
      return this.getCurrentState();
    }
    return null;
  }

  getCurrentState() {
    return this.history[this.currentIndex];
  }

  canUndo() {
    return this.currentIndex > 0;
  }

  canRedo() {
    return this.currentIndex < this.history.length - 1;
  }

  getHistory() {
    return {
      history: this.history,
      currentIndex: this.currentIndex,
    };
  }
}

// 使用示例: 文档编辑器
const editor = new HistoryManager({ content: "" });

// 执行一系列编辑
editor.execute((state) => ({ ...state, content: "Hello" }));
editor.execute((state) => ({ ...state, content: "Hello World" }));
editor.execute((state) => ({ ...state, content: "Hello World!" }));

console.log("当前:", editor.getCurrentState().content); // "Hello World!"

// 撤销两次
editor.undo();
editor.undo();
console.log("撤销后:", editor.getCurrentState().content); // "Hello"

// 重做一次
editor.redo();
console.log("重做后:", editor.getCurrentState().content); // "Hello World"

// 查看完整历史
console.log("历史记录:", editor.getHistory());

性能优化

1. 结构共享

虽然不可变性会创建新对象, 但可以通过结构共享来优化性能:

javascript
// 只有改变的部分会创建新对象, 未改变的部分共享引用
const original = {
  a: { value: 1 },
  b: { value: 2 },
  c: { value: 3 },
};

const updated = {
  ...original,
  a: { value: 10 }, // 只有 a 是新对象
};

console.log(original.a === updated.a); // false (改变了)
console.log(original.b === updated.b); // true  (共享引用)
console.log(original.c === updated.c); // true  (共享引用)

这意味着我们可以使用浅比较来快速检测变化:

javascript
// React.memo 或 shouldComponentUpdate 中的优化
function arePropsEqual(prevProps, nextProps) {
  // 浅比较就足够了, 因为不可变数据改变时引用会变
  return prevProps.data === nextProps.data;
}

2. 使用不可变库

对于复杂场景, 可以使用专门的不可变库如 Immer:

javascript
import { produce } from "immer";

const baseState = {
  users: [
    { id: 1, name: "John", age: 25 },
    { id: 2, name: "Jane", age: 30 },
  ],
  settings: {
    theme: "dark",
    notifications: true,
  },
};

// 使用 Immer, 可以用"可变"的语法, 但结果是不可变的
const nextState = produce(baseState, (draft) => {
  // 看起来像在修改, 实际上 Immer 会创建新对象
  draft.users.push({ id: 3, name: "Bob", age: 28 });
  draft.users[0].age = 26;
  draft.settings.theme = "light";
});

console.log(baseState.users.length); // 2 (未改变)
console.log(nextState.users.length); // 3
console.log(baseState === nextState); // false (不同的对象)

注意事项

1. 深层嵌套的处理

对于深层嵌套的对象, 手动更新会很繁琐:

javascript
// 深层嵌套更新
const state = {
  app: {
    ui: {
      sidebar: {
        width: 200,
        collapsed: false,
      },
    },
  },
};

// 繁琐但安全的方式
const updated = {
  ...state,
  app: {
    ...state.app,
    ui: {
      ...state.app.ui,
      sidebar: {
        ...state.app.ui.sidebar,
        collapsed: true,
      },
    },
  },
};

// 或者使用辅助函数
const setIn = (obj, path, value) => {
  const keys = path.split(".");
  const lastKey = keys.pop();

  const newObj = { ...obj };
  let current = newObj;

  for (const key of keys) {
    current[key] = { ...current[key] };
    current = current[key];
  }

  current[lastKey] = value;
  return newObj;
};

const result = setIn(state, "app.ui.sidebar.collapsed", true);

2. 数组操作的性能

频繁操作大数组时, 不可变方式可能影响性能:

javascript
// 性能考虑
const largeArray = Array.from({ length: 10000 }, (_, i) => i);

// 多次更新时, 每次都创建新数组
console.time("immutable");
let result = largeArray;
for (let i = 0; i < 100; i++) {
  result = [...result, result.length]; // 每次都复制整个数组
}
console.timeEnd("immutable");

// 如果需要多次更新, 考虑批量处理
console.time("batched");
const updates = Array.from({ length: 100 }, (_, i) => i);
const batchResult = [...largeArray, ...updates];
console.timeEnd("batched");

小结

不可变性是函数式编程的核心原则, 它通过禁止修改现有数据来:

  1. 消除副作用: 函数不会意外修改外部状态
  2. 简化调试: 数据流清晰, 易于追踪
  3. 支持时间旅行: 可以保存和回溯状态历史
  4. 优化性能: 通过引用比较快速检测变化
  5. 并发安全: 避免竞态条件

虽然不可变性会增加一些内存开销, 但通过结构共享和合理的库支持, 这些成本是可以接受的。在现代 JavaScript 应用中, 特别是使用 React、Redux 等框架时, 不可变性已经成为标准实践。

掌握不可变数据操作, 是写出可维护、可预测代码的关键一步。