不可变性(Immutability):为什么不能 push,而要用 concat?
——避免状态突变带来的“意料之外”的修改,保证数据流可追踪
“可变性是程序 bug 的‘量子纠缠’——
一处修改,处处崩溃,却不知源头在哪。”
一、一个致命的 push,引发的血案
js
const users = ['Alice', 'Bob'];
function addAdmin(list) {
list.push('Admin'); // 祸根在此
return list;
}
const admins = addAdmin(users);
console.log(users); // ['Alice', 'Bob', 'Admin']
console.log(admins); // ['Alice', 'Bob', 'Admin']users 被意外修改了!addAdmin 不再是“添加管理员”,而是“污染原始用户列表”。
这就是副作用(Side Effect):函数修改了外部状态。
二、不可变性的核心原则
永远不直接修改原始数据
每次“修改”都返回一个新对象
正确做法:用 concat
js
const users = ['Alice', 'Bob'];
function addAdmin(list) {
return list.concat('Admin'); // 返回新数组
}
const admins = addAdmin(users);
console.log(users); // ['Alice', 'Bob'] 原始数据完好
console.log(admins); // ['Alice', 'Bob', 'Admin'] 新数据concat 不修改原数组,而是创建一个新数组。
三、为什么 push 危险?—— 突变的三大罪状
罪状 1:破坏引用相等性(Reference Equality)
js
const original = { name: 'Alice' };
const updated = original;
updated.age = 25; // 突变
original === updated; // true —— 但数据已不同!你无法通过 === 判断数据是否变化,因为引用没变。
而在不可变世界:
js
const updated = { ...original, age: 25 };
original === updated; // false —— 引用不同,数据不同,逻辑一致罪状 2:导致“幽灵修改”(Ghost Mutation)
js
// 组件 A
const config = { theme: 'dark', lang: 'en' };
// 组件 B(第三方库)
function enhance(config) {
config.plugins = ['seo', 'analytics']; // 突变
return config;
}
// 组件 A 再次使用 config
renderApp(config); // 主题没变?不,plugins 被偷偷加了!突变让数据契约失效,你无法信任任何传入的对象。
罪状 3:破坏时间旅行与可追溯性
js
let state = { count: 0 };
function increment() {
state.count++; // 突变
}
increment();
increment();你想回溯 state 的历史?不可能。
因为 state 始终是同一个引用,过去的值已被覆盖。
而在 Redux 中:
js
// 每次返回新 state
const newState = { ...state, count: state.count + 1 };你可以轻松实现撤销、重做、时间旅行调试。
四、不可变性如何拯救代码?
1. 可预测的数据流
js
const nextState = reducer(prevState, action);你知道:
prevState不会被修改nextState是一个全新的状态- 变化是显式的、可追踪的
2. 安全的共享与传递
js
// 你可以放心地把数据传给子组件、API、worker
renderProfile(user); // 不用担心 profile 组件会修改 user因为你知道:任何“修改”都会返回新对象。
3. 高效的变更检测
在 React 中:
text
// 浅比较即可
shouldComponentUpdate(nextProps) {
return this.props.user !== nextProps.user;
}如果 user 是可变的,即使内容变了,引用也可能相同,导致更新遗漏。
五、JS 中实现不可变性的工具
1. 数组操作(避免 push/pop/splice)
| 可变方法 | 不可变替代 |
|---|---|
push | arr.concat(item) 或 [...arr, item] |
pop | arr.slice(0, -1) |
splice | arr.filter(...) 或 arr.slice() + concat |
js
const newArr = [...oldArr.slice(0, i), newItem, ...oldArr.slice(i+1)];2. 对象操作(避免直接赋值)
js
//
obj.name = 'New';
//
const newObj = { ...obj, name: 'New' };3. 深层更新
js
// 使用 immer(推荐)或递归
import produce from 'immer';
const nextState = produce(state, draft => {
draft.users[0].age = 26; // 看似突变,实则生成新对象
});六、不可变性的“性能焦虑”与真相
“每次创建新对象,不会很慢吗?”
真相:现代 JS 引擎和函数式库已优化:
- 结构共享(Structural Sharing):新旧对象共享未变部分
- 惰性求值:只在需要时才计算
- V8 优化:短生命周期对象的 GC 非常高效
示例:Immutable.js 的树结构
js
// 两个数组共享大部分节点,只新增一条路径
const list1 = Immutable.List([1,2,3,4,5]);
const list2 = list1.set(2, 99); // 只创建新分支,其他节点复用七、工程实践:拥抱不可变性
1. 使用 ESLint 规则禁止突变
text
// eslint-plugin-functional
'functional/no-mutating-methods': 'error'2. 使用 const + 不可变模式
js
const users = ['Alice'];
// const 防止重新赋值,但不防突变
// 需配合编程习惯3. 在 Redux、React Hooks 中强制使用
js
setState(prev => ({ ...prev, count: prev.count + 1 }));结语:不可变性是“函数式编程的基石”
它让程序从“状态机的混沌”走向“数据流的秩序”。
concat 不是语法选择,而是一种哲学:
- 数据是值(Value),不是变量(Variable)
- 变化是转换(Transformation),不是修改(Mutation)
- 函数是映射(Map),不是指令(Command)
当你放弃 push,你失去的只是一时的便利;
你获得的,是可预测、可测试、可追溯的代码宇宙。