高阶函数玩转AOP:前端开发者轻松实现切面编程(附ES6实战)

在这里插入图片描述

高阶函数玩转AOP:前端开发者轻松实现切面编程(附ES6实战)

引言:当“写代码”变成“考古现场”

上周五晚上十点,我正准备关机跑路,产品突然在群里甩了一张截图:
“为什么这个按钮点了没埋点?上周不是加过了吗?”
我点开代码,发现那个按钮的回调里,日志语句被另一个同事注释掉了——理由是“影响阅读”。
那一刻,我深刻体会到什么叫“现代前端代码的慢性自杀”:
日志、权限、埋点、错误处理……这些“横切逻辑”像牛皮癣一样贴在业务函数身上,复制粘贴得比微博热搜还快,改起来却比前任的心还难捂热。

如果你也曾在凌晨两点,对着一个 300 行的 handleSubmit 怀疑人生,那么今天这篇“长篇”就是为你写的。
我们要用 JavaScript 最性感的特性——高阶函数,给代码做一次“微创整容”:
不碰业务,只贴“面膜”,让日志、权限、埋点这些横切关注点像面膜一样敷上去,撕下来时也不带掉一根业务头发。
这套手法,在江湖上被称为 AOP(Aspect-Oriented Programming,面向切面编程)
而高阶函数,正是我们手里那把“刮骨疗伤”的小刀。

JavaScript 高阶函数:别再把“回调”当成全部

“高阶函数”四个字,很多人第一反应就是“把函数当参数传”。
这说法就像“谈恋爱就是请吃饭”一样,Too young。
官方定义:满足以下任意一条即可

  1. 接收一个或多个函数作为参数
  2. 返回一个函数

看到没?返回函数才是最容易被忽视的大招。
把函数当成乐高积木,返回、组合、延迟执行,才是高阶函数真正的“成人礼”。

下面这三板斧,先帮你热热身:

// 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
支持 beforeafteraround 三种切面,零依赖、可树摇、可插拔
让你的业务函数瞬间拥有“乐高铠甲”。

// 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.applycall 显式传 this
或者全部箭头函数(没有自己的 this),但箭头函数又无法 new
终极方案:让切面库内部统一用 function保持 thisArg
如上文 mini-aop.jssafeApply 实现。

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. 性能“千层饼”

过度堆叠切面,函数调用栈像千层饼
低端安卓机直呼“烫手”。
优化思路:

  1. 生产环境用 babel-plugin 静态编译掉不必要的切面;
  2. 把多个小切面合成一个,减少层层包裹;
  3. 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❌ 不建议业务规则本身,硬编码更清晰
性能要求极高的底层库⚠️ 谨慎多一层函数调用,低端机可能掉帧
团队新手多⚠️ 渐进式先培训,再推广,避免“魔法代码”恐慌

团队协作规范(血泪总结):

  1. 统一目录:所有切面放 aspects/ ,禁止散落到业务文件夹;
  2. 命名前缀withLogwithPermission,一眼识别;
  3. 禁止嵌套超过 3 层:再多就“俄罗斯套娃”,code review 直接打回;
  4. 文档先行:每个切面必须写 JSDoc,说明输入输出、副作用,
    否则下一个接盘侠会在群里@你

结语:AOP 不是“炫技”,是“减负”

写这篇文章时,我特意翻了自己三年前的提交记录——
那满屏的 console.logif (!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等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DTcode7

客官,赏个铜板吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值