JavaScript AOP 之 around 环绕通知实现详解
本文深入解析 JavaScript 中面向切面编程(AOP)的核心机制,重点演示如何手动实现 around 环绕通知。通过逐步重构代码,解决作用域丢失、无限递归及移除绑定等关键问题,最终构建出具备 remove 功能的健壮 AOP 模块,提升前端代码的可维护性与灵活性。
AOP 在 JavaScript 中的应用背景
AOP(面向切面编程)中的“通知”是切面的具体实现形式,主要包含 before(前置通知)、after(后置通知)以及 around(环绕通知)。熟悉 Spring 框架的开发者对其并不陌生,但在 JavaScript 领域,AOP 往往被忽视。实际上,利用 AOP 能有效优化 JS 代码逻辑,例如在 Dojo 和 YUI3 等前端框架中,AOP 已被提升为自定义事件的核心机制,源码中随处可见其身影。这种抽象设计使得 Dojo 的自定义事件具备极高的强大性与灵活性。Dojo 的 AOP 实现在 dojo/aspect 模块中,主要提供 before、after 和 around 三个方法。本文将逐步引导读者实现 around 方法,后续文章将深入解析 dojo/aspect 模块的整体架构。
环绕通知的基础实现
在 JavaScript 中实现环绕通知,最直观且基础的方式是利用回调函数(callback)。
advice = function(originalFunc){
console.log("before function");
originalFunc();
console.log("after function");
}
var obj = {
foo: function(){
console.log('foo');
}
}
advice(obj.foo)
运行结果如下:
before function foo after function 看起来非常简单,难道这就结束了吗?
引入闭包解决作用域问题
然而,这样的实现略显粗糙。所谓的“环绕”应当具备更强的复用性,例如再次调用 obj.foo 时,结果不应仅仅是打印“foo”,而应保持一致的环绕行为。为此,我们需要对代码进行改进,引入闭包机制。
advice = function(originalFunc){
return function() {
console.log("before function");
originalFunc();
console.log("after function");
}
}
var obj = {
foo: function(){
console.log(this.name);
},
name: "obj"
}
obj.foo = advice(obj.foo)
obj.foo()
输出结果:
before function after function
虽然达到了环绕的效果,但之前提到的 name 属性去哪里了?
在 advice 返回的闭包中,我们必须妥善处理作用域问题。
advice = function(originalFunc){
return function() {
console.log("before function");
originalFunc();
console.log("after function");
}
}
var obj = {
foo: function(){
console.log(this.name);
},
name: "obj"
}
keepContext = function() {
return obj['foo'].call(obj);
}
obj.foo = advice(keepContext);
看似利用 call 方法解决了作用域问题,让我们运行代码验证一下:

这难道就是传说中的死循环?
看来需要调整思路,借助中间变量来消除死循环。
advice = function(originalFunc){
return function() {
console.log("before function");
originalFunc();
console.log("after function");
}
}
var obj = {
foo: function(){
console.log(this.name);
},
name: "obj"
}
var exist = obj.foo;
keepContext = function() {
return exist.call(obj);
}
obj.foo = advice(keepContext);
obj.foo();
输出:
before function obj after function
around 方法的抽象与解耦
世界突然变得美好了。但是,这一堆代码看起来是否过于简陋?我们需要进行更高级的抽象,我也这么认为。
function around(obj, prop, advice){
var exist = obj[prop];
var advised = advice(function(){
return exist.call(obj, arguments);
});
obj[prop] = advised;
}
advice = function(originalFunc){
return function() {
console.log("before function");
originalFunc();
console.log("after function");
}
}
var obj = {
foo: function(){
console.log(this.name);
},
name: "obj"
}
around(obj, 'foo', advice);
obj.foo();
around 方法将处理过程与具体对象解耦;advice 只需按照特定格式书写,即可实现 around 的效果。
advice = function(originalFunc){
return function() {
//before
originalFunc();
//after
}
}
瞬间变得高大上、炫酷十足,不是吗?
实现 remove 功能以支持动态绑定
那么问题来了:如果不小心多次调用 around 方法该怎么办?这是一个值得思考的问题。我们是否应该让 around 返回一个句柄,其中包含 remove 方法,用于解除绑定,就像事件绑定与移除机制一样。
所谓 remove,即让函数在下一次执行时不再执行对应的 around 方法,而仅运行 originalFunc 方法。
function around(obj, prop, advice){
var exist = obj[prop];
var previous = function(){
return exist.call(obj, arguments);
};
var advised = advice(previous);
obj[prop] = advised;
return {
remove: function(){
obj[prop] = exist;
advice = null;
previous = null;
exist = null;
obj = null;
}
}
}
var count = 1;
advice = function(originalFunc){
var current = count++;
return function() {
console.log("before function " + current);
originalFunc(arguments);
console.log("after function " + current);
}
}
var obj = {
foo: function(arg){
console.log(this.name + " and " + arg);
},
name: "obj"
}
h1 = around(obj, 'foo', advice);
h2 = around(obj, 'foo', advice);
obj.foo();
h1.remove();
obj.foo();
h2.remove();
obj.foo();
输出:
before function 2
before function 1
obj and [object Arguments]
after function 1
after function 2
obj and undefined
before function 1

结果不仅混乱,还报错了。这不可接受,必须解决。
啊,闭包……请赐予我力量吧!
function around(obj, prop, advice){
var exist = obj[prop];
var previous = function(){
return exist.apply(obj, arguments);
};
var advised = advice(previous);
obj[prop] = function(){
//当调用remove后,advised为空
//利用闭包的作用域链中可以访问到advised跟previous变量,根据advised是否为空可以来决定调用谁
return advised ? advised.apply(obj, arguments) : previous.apply(obj, arguments);
};
return {
remove: function(){
//利用闭包的作用域链,在remove时将advised置空,这样执行过程中不会进入本次around
//这几个不能删
//obj[prop] = exist;
advised = null;
advice = null;
//previous = null;
//exist = null;
//obj = null;
}
}
}
var count = 1;
advice = function(originalFunc){
var current = count++;
return function() {
console.log("before function " + current);
originalFunc.apply(this, arguments);
console.log("after function " + current);
}
}
var obj = {
foo: function(arg){
console.log(this.name + " and " + arg);
},
name: "obj"
}
h1 = around(obj, 'foo', advice);
h2 = around(obj, 'foo', advice);
obj.foo('hello world');
h1.remove();
obj.foo('hello world');
h2.remove();
obj.foo('hello world');
输出:
before function 2
before function 1
obj and hello world
after function 1
after function 2
before function 2
obj and hello world
after function 2
obj and hello world
打完,收功!
第一次通宵写博客的经历令人难忘:两点听到隔壁的声响,四点听到乌鸦啼鸣,五点时各种鸟叫声此起彼伏。
参考文章:
使用 AOP 改善 javascript 代码
yui3 的 AOP(面向切面编程)和 OOP(面向对象编程)
面向切面编程(AOP)的理解
JavaScript异步发送HTTP请求时怎么优雅地处理错误
2026-11-05 17:10:44
5款主流JS在线运行工具对比:CodePen、JSFiddle与StackBlitz怎么选
2026-11-05 17:04:04
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
