在JavaScript函数式编程里,Monad并不是某个具体的内置对象,而是一类满足特定代数定律的容器结构。它把值包装进上下文,并提供统一的操作接口,让带有副作用或不确定性的计算能够像普通值一样被组合。理解Monad的关键,在于明白它如何解决嵌套结构和错误处理中的胶水代码问题。

什么是Monad:从容器与定律说起
Monad本质上是一个包装了值的对象,同时附带了两个核心操作:一个用于把普通值放进容器的函数,通常叫unit或of;另一个用于把容器里的值拿出来并继续计算的函数,通常叫flatMap或bind。只要某个类型实现了这两个操作,并且遵守同一律和结合律,它在函数式编程里就可以被称为Monad。
在JavaScript中,我们完全可以用普通类来模拟。比如一个简单的Identity容器,它只是把值存起来,不做额外处理。这种最基础的Monad能帮助理解“值被装在盒子里”这一心智模型。后续所有的错误处理、异步流程,都是在这个盒子上增加不同的上下文规则。
class Identity {
constructor(value) {
this.value = value;
}
// unit操作,把值装进Monad
static of(value) {
return new Identity(value);
}
// map不改变容器结构,只变换内部值
map(fn) {
return new Identity(fn(this.value));
}
// flatMap取出值计算,并返回新的Monad
flatMap(fn) {
return fn(this.value);
}
}
const result = Identity.of(3)
.map(x => x + 2)
.flatMap(x => Identity.of(x * 3));
console.log(result.value); // 输出15
Maybe Monad:优雅处理空值
日常开发里最头疼的往往是null和undefined带来的嵌套判空。Maybe Monad通过区分Just和Nothing两种状态,把“是否有值”这件事交给容器自己管理。调用方不再写大量的if判断,而是用统一的map或flatMap来描述后续逻辑,容器自动在空值时短路。
下面这段实现中,Maybe.of会在入参为空时返回Nothing实例。后续的map调用在Nothing上直接原样返回,不会执行函数体,从而避免了Cannot read property错误。相比层层if (obj && obj.x && obj.x.y)的写法,链式调用明显更干净,也更容易测试。
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return value === null || value === undefined
? new Nothing()
: new Just(value);
}
map(fn) { return this; }
flatMap(fn) { return this; }
}
class Just extends Maybe {
map(fn) {
return Maybe.of(fn(this.value));
}
flatMap(fn) {
return fn(this.value);
}
}
class Nothing extends Maybe {}
const getUserCity = user =>
Maybe.of(user)
.map(u => u.address)
.map(a => a.city)
.map(c => c.toUpperCase());
console.log(getUserCity({ address: { city: 'beijing' } }).value);
// 输出 BEIJING
console.log(getUserCity(null).value);
// 输出 undefined,无报错
Promise也是一种Monad
很多开发者没意识到,JavaScript原生的Promise其实符合Monad的特征。它的resolve相当于unit,而then在返回Promise时会自动展开,相当于flatMap。这也是为什么我们可以在then里直接return另一个异步操作,而不用自己写嵌套回调。
对比回调地狱,Promise用链式结构把异步上下文封装起来。虽然标准里then同时承担了map和flatMap的职责,但从组合角度看,它让异步代码拥有了同步写法般的可读性。理解这一点后,再去看RxJS的Observable或Task类型,会发现它们都在复用同一套Monad思路。
const fetchUser = id =>
Promise.resolve({ id, name: 'tom' });
const fetchOrder = user =>
Promise.resolve({ uid: user.id, price: 99 });
// then返回Promise,自动扁平化,类似flatMap
fetchUser(1)
.then(fetchOrder)
.then(order => console.log(order));
// 输出 { uid: 1, price: 99 }
自己实现一个Either Monad做错误处理
Either Monad适合显式表达“成功或失败”的分支。它包含Left和Right两个子类,通常约定Left承载错误信息,Right承载正常结果。和Maybe不同,Either不会悄悄丢掉错误,而是把错误一路传下去,方便集中处理。
在下面的代码里,safeJsonParse返回Either,如果解析失败就包成Left。后续计算只在Right上执行,Left直接跳过。这样业务函数可以保持纯函数风格,所有异常都被收敛到容器层面,调用方通过isLeft来决定怎么提示用户。
class Either {
constructor(value) { this.value = value; }
static right(v) { return new Right(v); }
static left(v) { return new Left(v); }
map() { return this; }
flatMap() { return this; }
}
class Right extends Either {
map(fn) { return Either.right(fn(this.value)); }
flatMap(fn) { return fn(this.value); }
}
class Left extends Either {}
const safeJsonParse = str => {
try {
return Either.right(JSON.parse(str));
} catch (e) {
return Either.left('解析失败: ' + e.message);
}
};
const r = safeJsonParse('{"a":1}')
.map(o => o.a + 10);
console.log(r.value); // 11
const l = safeJsonParse('bad')
.map(o => o.a + 10);
console.log(l.value); // 解析失败: Unexpected token b
实践建议与常见误区
初学Monad容易陷入两个极端:一是认为必须严格证明定律才可使用,结果迟迟不敢写;二是滥用Monad把简单逻辑包得层层叠叠。实际上在JavaScript业务里,只要你的容器能稳定做链式变换并隐藏上下文,就已经收获了Monad的主要价值。
建议从Maybe和Either入手,把现有代码里散落的null判断和try-catch逐步替换成容器操作。等团队习惯flatMap思维后,再引入Task或IO类型处理副作用。记住,Monad是工具不是目的,它帮我们减少胶水代码、提升组合性,而不是增加理解负担。
| Monad类型 | 解决的主要问题 | 核心方法 |
|---|---|---|
| Identity | 理解容器模型 | map, flatMap |
| Maybe | 空值短路 | of, map |
| Either | 显式错误处理 | left, right |
| Promise | 异步扁平化 | then, resolve |
通过上述几种Monad的对比可以看到,它们共享同一套组合语言,只是上下文不同。掌握这套语言后,你在JavaScript里写函数式代码会更加自如,也能更清楚地向同事解释为什么一段链式调用既安全又易维护。
JavaScript函数式编程Monad修改时间:2026-08-09 16:27:39