JavaScript虽非纯函数式语言,但可通过规避副作用、坚持不可变性、避免共享状态来写出接近纯净的函数式代码;纯净函数要求相同输入恒得相同输出且无任何可观察副作用。

JavaScript 本身不是纯函数式语言,但你可以写出接近纯净的函数式代码——关键不在“能不能”,而在“是否主动规避副作用、是否坚持不可变性、是否避免共享状态”。
什么是纯净函数?先看一个反例
纯净函数必须满足两个条件:相同输入永远返回相同输出;不产生任何可观察的副作用(比如修改全局变量、发请求、操作 DOM、修改入参)。
常见污染点:Array.prototype.push、Array.prototype.sort、Object.assign(当第一个参数是原对象时)、直接赋值 obj.key = value。
错误写法示例:
立即学习“Java免费学习笔记(深入)”;
const addTodo = (list, text) => {
list.push({ id: Date.now(), text }); // ❌ 修改了原数组
return list;
};
正确写法应返回新数组:
const addTodo = (list, text) => [
...list,
{ id: Date.now(), text }
];
用 const 和不可变数据结构约束自己
const 只保证绑定不被重新赋值,不阻止对象/数组内部被修改。所以得靠习惯和工具辅助。
- 默认用扩展运算符
[...arr]、{...obj}创建新副本 - 用
map、filter、reduce替代for循环(它们天然不修改原数组) - 对深层嵌套对象,避免手写深拷贝;小项目可用
JSON.parse(JSON.stringify(obj))(仅限简单数据),大项目考虑immer或lodash/fp的set/update - 函数参数不加
default值以外的赋值逻辑,比如避免(x = x || {})—— 这会掩盖传入null或0的意图
如何处理真实世界里的副作用?
HTTP 请求、时间、随机数、DOM 操作这些无法消除,但可以隔离。
把副作用“推到边缘”:
- 把 API 调用封装进单独函数,如
fetchUser(id),它只负责发起请求,不处理渲染或状态更新 - 用高阶函数接收回调,而非在函数内部调用
console.log或document.querySelector - 异步流程优先用
async/await+Promise链,避免在then中直接修改外部变量 - 如果用 Redux,确保所有 reducer 是纯净的;副作用交给
redux-thunk或redux-saga处理
例如,这个函数就混杂了逻辑与副作用:
const loadProfile = async (id) => {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
document.getElementById('name').innerText = data.name; // ❌ 侵入 DOM
return data;
};
应该拆成:
const fetchProfile = async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json(); // ✅ 只做数据获取
};
// 使用方决定怎么渲染
fetchProfile(123).then(data => {
document.getElementById('name').innerText = data.name;
});
工具链能帮你守住底线吗?
不能完全替代意识,但能显著降低出错概率。
- ESLint 插件
eslint-plugin-functional可检测mutating props、no-let、no-loop-statement等 - TypeScript 配合
readonly修饰符(如readonly items: string[])能在编译期提示非法修改 - 运行时防护:小项目可用
deep-freeze在开发环境冻结 props/state,一旦被改就抛错 - 注意:Babel 编译后的代码可能绕过某些检查,别盲目信任构建产物
最常被忽略的一点:函数式风格不等于堆砌 compose 和 pipe。过度抽象会让错误堆栈难读、调试变慢。该直白时就直白,尤其在边界处(I/O、事件处理、初始化逻辑)。











