
高阶函数玩转AOP:前端开发者轻松实现切面编程(附ES6实战)
高阶函数玩转AOP:前端开发者轻松实现切面编程(附ES6实战)
引言:当“写代码”变成“考古现场”
上周五晚上十点,我正准备关机跑路,产品突然在群里甩了一张截图:
“为什么这个按钮点了没埋点?上周不是加过了吗?”
我点开代码,发现那个按钮的回调里,日志语句被另一个同事注释掉了——理由是“影响阅读”。
那一刻,我深刻体会到什么叫“现代前端代码的慢性自杀”:
日志、权限、埋点、错误处理……这些“横切逻辑”像牛皮癣一样贴在业务函数身上,复制粘贴得比微博热搜还快,改起来却比前任的心还难捂热。
如果你也曾在凌晨两点,对着一个 300 行的 handleSubmit 怀疑人生,那么今天这篇“长篇”就是为你写的。
我们要用 JavaScript 最性感的特性——高阶函数,给代码做一次“微创整容”:
不碰业务,只贴“面膜”,让日志、权限、埋点这些横切关注点像面膜一样敷上去,撕下来时也不带掉一根业务头发。
这套手法,在江湖上被称为 AOP(Aspect-Oriented Programming,面向切面编程)。
而高阶函数,正是我们手里那把“刮骨疗伤”的小刀。
JavaScript 高阶函数:别再把“回调”当成全部
“高阶函数”四个字,很多人第一反应就是“把函数当参数传”。
这说法就像“谈恋爱就是请吃饭”一样,Too young。
官方定义:满足以下任意一条即可
- 接收一个或多个函数作为参数
- 返回一个函数
看到没?返回函数才是最容易被忽视的大招。
把函数当成乐高积木,返回、组合、延迟执行,才是高阶函数真正的“成人礼”。
下面这三板斧,先帮你热热身:
// 1. 返回函数——“延迟执行”
const add = x => y => x + y;
const add5 = add(5);
console.log(add5(3)); // 8
// 2. 组合函数——“乐高式拼接”
const compose = (...fns) => val => fns.reduceRight((v, fn) => fn(v), val);
const shout = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const scream = compose(exclaim, shout);
console.log(scream('hello')); // HELLO!
// 3. 拦截函数——“偷梁换柱”
const withLog = fn => (...args) => {
console.log(`[LOG] 调用${fn.name},参数`, args);
const ret = fn(...args);
console.log(`[LOG] ${fn.name}返回`, ret);
return ret;
};
把这三招融会贯通,你就拥有了“切面”最原始的手术刀:
不碰原函数,却能在外面套一层“壳”。
而这,正是 AOP 的底层逻辑。
AOP 思想初探:给代码贴“创可贴”,而不是“开刀”
想象一个场景:
async function createOrder(params) {
// 1. 权限校验
if (!hasPermission('order:add')) throw new Error('没权限');
// 2. 埋点
track('order_click', params);
// 3. 真正业务
const res = await fetch('/api/order', { body: JSON.stringify(params) });
// 4. 异常处理
if (!res.ok) reportError(res);
return res.json();
}
上面的代码,业务核心其实只有一行 fetch。
其余都是“横切关注点”,它们像鼻涕一样糊在业务脸上。
AOP 的思路是:把鼻涕换成透明面膜,敷上去,撕下来,业务还是那张脸。
关键词:不修改原函数,可插拔,可复用。
高阶函数恰好能完美实现:
返回一个新函数,把原函数包在里面,想加什么加什么,想拆随时拆。
手撸一个轻量级 AOP 工具库:before、after、around 三剑客
下面我们用 ES6 的“十八般武艺”,写一个只有 60 行代码的 mini-aop.js,
支持 before、after、around 三种切面,零依赖、可树摇、可插拔,
让你的业务函数瞬间拥有“乐高铠甲”。
// mini-aop.js
const isAsync = fn => fn.constructor.name === 'AsyncFunction';
// 工具:保证this指向,兼容async
function safeApply(fn, thisArg, args) {
// async函数需要特殊处理,避免Promise链断裂
const ret = fn.apply(thisArg, args);
return ret && typeof ret.then === 'function' ? ret : Promise.resolve(ret);
}
// before切面:原函数前先执行
export function before(beforeFn) {
return function(targetFn) {
return function(...args) {
// before里可以改参数,也可以提前中断
const newArgs = beforeFn.apply(this, args) || args;
return targetFn.apply(this, newArgs);
};
};
}
// after切面:原函数后执行,可以改返回值
export function after(afterFn) {
return function(targetFn) {
return function(...args) {
const ret = targetFn.apply(this, args);
// 兼容async
if (ret && typeof ret.then === 'function') {
return ret.then(data => afterFn.call(this, data, args) || data);
}
return afterFn.call(this, ret, args) || ret;
};
};
}
// around切面:完全包裹,自定义是否调用原函数
export function around(aroundFn) {
return function(targetFn) {
return function(...args) {
const invoke = () => safeApply(targetFn, this, args);
return aroundFn.call(this, { invoke, args, target: targetFn });
};
};
}
// 组合工具:支持多个切面串行
export function composeAspects(...aspects) {
return targetFn => aspects.reduceRight((acc, asp) => asp(acc), targetFn);
}
使用姿势极其丝滑:
import { before, after, around, composeAspects } from './mini-aop.js';
// 原函数
const pay = async (orderId, amount) => {
const res = await fetch('/api/pay', { method: 'POST', body: JSON.stringify({ orderId, amount }) });
return res.json();
};
// 切面1:权限
const checkPermission = before(function(orderId, amount) {
if (!window.currentUser.canPay) throw new Error('无权支付');
console.log(`[before] 权限校验通过,用户:${window.currentUser.name}`);
});
// 切面2:埋点
const trackPay = after(function(result, [orderId, amount]) {
console.log(`[after] 埋点:用户支付成功,订单${orderId},金额${amount}`);
window.gtag && gtag('event', 'pay_success', { value: amount });
});
// 切面3:loading
const withLoading = around(async ({ invoke }) => {
Toast.show('支付中...');
try {
const data = await invoke();
Toast.show('支付成功');
return data;
} catch (e) {
Toast.show('支付失败');
throw e;
}
});
// 组合:乐高式拼装
const finalPay = composeAspects(checkPermission, trackPay, withLoading)(pay);
// 调用
document.querySelector('#payBtn').onclick = () => finalPay('12345', 699);
看到没?原函数 pay 一行没动,却拥有了权限、埋点、loading 三大能力。
想卸掉哪个,只需把对应切面从 composeAspects 里删掉,业务0感知。
真实战场:三个让老板眼前一亮的案例
1. 用户行为埋点:全自动“爬虫”式采集
需求:页面上所有按钮的点击,都想带上 data-track 属性,自动上报,开发0改动。
传统做法:给每个按钮绑 onclick?想辞职直说。
AOP 方案:劫持 addEventListener,一次搞定全站。
// track-hijack.js
const rawAEL = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (type === 'click' && this.dataset.track) {
// 包裹原listener
const wrapped = function(evt) {
// 先上报
window.gtag && gtag('event', this.dataset.track, {
tag: this.tagName,
id: this.id,
text: this.innerText?.slice(0, 50)
});
// 再执行原逻辑
return listener.call(this, evt);
};
return rawAEL.call(this, type, wrapped, options);
}
return rawAEL.call(this, type, listener, options);
};
上线后,产品自己往按钮上写 data-track="register_click" 就能收到数据,
前端再也不用当“埋点奴隶”。
2. 统一错误处理:接口炸了也能优雅“甩锅”
需求:所有 fetch 调用,只要非 200,统一弹窗 + 上报 Sentry。
传统做法:每个 fetch 后面加 if (!res.ok)?累不累?
AOP 方案:包裹全局 fetch,一次埋雷,终生免疫。
// error-intercept.js
const originalFetch = window.fetch;
window.fetch = async (...args) => {
try {
const res = await originalFetch(...args);
if (!res.ok) {
// 统一错误弹窗
Toast.error(`服务器开小差啦~(${res.status})`);
// 上报
Sentry.captureException(new Error(`FetchError ${res.status}`), {
extra: { url: args[0] }
});
}
return res;
} catch (err) {
// 网络断网等异常
Toast.error('网络似乎走丢了~');
Sentry.captureException(err);
throw err;
}
};
从此,后端返回 500,前端用户看到的不再是“白板”,而是贴心提示;
而你,也能在 Sentry dashboard 里优雅甩锅。
3. 权限拦截:干掉满屏的 if (!hasPermission)
需求:某些按钮/接口,只有管理员可见/可调用。
传统做法:复制 if 到天荒地老。
AOP 方案:给函数贴“封条”,无权限直接短路。
// permission-aspect.js
export const requireRole = role => before(function(...args) {
if (!window.currentUser.roles.includes(role)) {
Message.warn('您没有权限执行该操作');
// 中断后续
throw new Error('FORBIDDEN');
}
});
// 使用
import { requireRole } from './permission-aspect.js';
const deleteUser = async id => fetch(`/api/user/${id}`, { method: 'DELETE' });
// 只有超管能删人
const safeDeleteUser = requireRole('superadmin')(deleteUser);
配合路由守卫,前端也能把“越权”扼杀在摇篮里,
再也不用把希望寄托在后端那个“可能忘记验权”的接口上。
踩坑实录:AOP 不是银弹,是“双面刃”
1. this 指向“蒸发”惨案
问题:
const obj = {
name: 'Alice',
speak: after(() => console.log('after'))(function() {
console.log(`I am ${this.name}`);
})
};
obj.speak(); // I am undefined
原因:after 返回的新函数,this 被重新绑定。
解法:在 after 内部使用 Reflect.apply 或 call 显式传 this,
或者全部箭头函数(没有自己的 this),但箭头函数又无法 new,
终极方案:让切面库内部统一用 function,保持 thisArg,
如上文 mini-aop.js 的 safeApply 实现。
2. 异步 around 链断裂
问题:
const wrap = around(({ invoke }) => {
console.log('before');
invoke(); // 忘记return
console.log('after');
});
invoke 返回的是 Promise,不 return 会导致外层拿不到结果,
async/await 会报错 undefined。
解法:始终 return invoke(),或 await invoke()。
3. 性能“千层饼”
过度堆叠切面,函数调用栈像千层饼,
低端安卓机直呼“烫手”。
优化思路:
- 生产环境用
babel-plugin静态编译掉不必要的切面; - 把多个小切面合成一个,减少层层包裹;
- 用
WeakMap做缓存,同一函数多次装饰只生成一次代理。
进阶玩法:让切面“更智能”
1. 装饰器语法糖:TypeScript 真香
如果你用 TypeScript,直接把 before/after 写成装饰器,
类方法也能无痛切面:
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const ori = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[LOG] ${key} called with`, args);
return ori.apply(this, args);
};
}
class OrderService {
@Log
async create(order: Order) {
// 业务代码
}
}
编译后装饰器逻辑与业务完全解耦,
重构、单测、回滚都爽到飞起。
2. 结合自定义事件:跨模块“广播”
切面里抛出自定义事件,让任何角落都能监听:
const broadcast = after(function(result) {
window.dispatchEvent(new CustomEvent('order:created', { detail: result }));
});
// 其他地方
window.addEventListener('order:created', e => refreshList(e.detail));
无需 import 模块,彻底解耦,
妈妈再也不用担心“循环依赖”。
3. 单元测试:Mock 切面 or 绕过?
策略 A:Mock 切面
jest.mock('./mini-aop.js', () => ({
after: () => fn => fn, // 直接返回原函数
}));
优点:测试代码干净;缺点:无法测切面本身。
策略 B:绕过切面
直接 import 原函数,不经过包裹:
import { pay } from './pay.js'; // 不是 finalPay
优点:切面逻辑可独立测试;缺点:需要 export 两份。
团队建议:关键业务用策略 B,切面逻辑也写单测,
双保险,上线不怂。
什么时候该用 AOP?一张“避坑”决策表
| 场景 | 建议 | 理由 |
|---|---|---|
| 日志、埋点、权限、错误处理 | ✅ 强烈推荐 | 典型横切关注点,AOP 天作之合 |
| 纯业务 if/else | ❌ 不建议 | 业务规则本身,硬编码更清晰 |
| 性能要求极高的底层库 | ⚠️ 谨慎 | 多一层函数调用,低端机可能掉帧 |
| 团队新手多 | ⚠️ 渐进式 | 先培训,再推广,避免“魔法代码”恐慌 |
团队协作规范(血泪总结):
- 统一目录:所有切面放
aspects/,禁止散落到业务文件夹; - 命名前缀:
withLog、withPermission,一眼识别; - 禁止嵌套超过 3 层:再多就“俄罗斯套娃”,code review 直接打回;
- 文档先行:每个切面必须写 JSDoc,说明输入输出、副作用,
否则下一个接盘侠会在群里@你。
结语:AOP 不是“炫技”,是“减负”
写这篇文章时,我特意翻了自己三年前的提交记录——
那满屏的 console.log、if (!hasPermission) 看得我头皮发麻。
如果当时就有人告诉我:
“嘿,别把‘非业务’的垃圾和业务裹在一起,用高阶函数撕下来!”
也许我就不用在无数个深夜,一边改埋点,一边骂“哪个傻X写的”。
如今,我把这套“高阶函数 + AOP”的手术刀递给你。
愿你的业务代码,从此只关心业务;
愿你的横切逻辑,像面膜一样即贴即撕;
愿下一个接你盘的人,看完代码后,
默默给你点了个 Star,而不是在群里@你。
刀已出鞘,乐高已摊开,
接下来,轮到你把代码玩成艺术品了。
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 | |
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

&spm=1001.2101.3001.5002&articleId=156668805&d=1&t=3&u=3bbe5e73f6404e94b5908d1a0b61b144)
3541

被折叠的 条评论
为什么被折叠?



