JavaScript装饰者模式:从基础实现到AOP扩展与应用
本文深入解析JavaScript中的装饰者模式,探讨如何在保持原有函数逻辑不变的前提下动态添加新功能。通过对比传统对象装饰与基于Function.prototype的AOP实现(before/after方法),详细阐述其执行顺序与this指向处理,并结合用户数据上报场景提供完整代码示例,帮助开发者降低耦合度并提升代码可维护性。
装饰者模式概述与问题背景
在JavaScript函数开发过程中,若需为现有函数增加与其核心功能无关的新特性,常规做法往往是在原函数内部直接追加相关代码。这种做法虽能实现功能,但由于新增逻辑与原有逻辑缺乏内在关联,会导致后期维护成本显著上升,并引发函数结构臃肿的问题。
更为优雅的方案是引入装饰器模式。该模式的核心在于保持现有函数及其内部实现代码的完整性,将新增功能封装为独立的函数,并通过包装机制与原函数协同执行,从而实现功能的动态扩展。
首先,我们来看一个基础的JavaScript版装饰者模式实现示例。
基础对象装饰器实现
var Plane = function(){}
Plane.prototype.fire = function(){
console.log('发射普通子弹');
}
//增加两个装饰类,导弹类和原子弹类
var MissileDecorator = function(plane){
this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
this.plane.fire();
console.log('发射导弹');
}
var AtomDecorator = function(plane){
this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
this.plane.fire();
console.log('发射原子弹');
}
var plane = new Plane();
console.log(plane);
plane = new MissileDecorator(plane);
console.log(plane);
plane = new AtomDecorator(plane);
console.log(plane);
plane.fire();
/*
发射普通子弹
发射导弹
发射原子弹
*/
接下来展示一种升级版的装饰器模式实现方式,该方法通过为JavaScript的Function构造函数添加实例方法before和after来达成目标。
Function.prototype.before和Function.prototype.after方法接收一个函数作为参数,该参数即为承载新增功能代码的新函数。
在方法内部,首先保存当前的this引用。由于Function是JavaScript中所有函数的构造器,因此Function.prototype中的this指向具体的实例函数,保存该引用有助于后续正确调用原函数。
随后,返回一个代理函数。需要注意的是,此处的“代理”仅指结构上的相似性,并不承担控制对象访问等传统代理职责。其核心工作是将请求分别转发给新添加的函数和原函数,并确保执行顺序:前置装饰(Function.prototype.before)确保新函数在原函数之前执行,后置装饰(Function.prototype.after)则相反,以此实现动态装饰效果。
基于Function原型的AOP装饰器
// AOP 装饰函数
Function.prototype.before = function(beforefn){
var _self = this; //保存原函数的引用
return function(){ //返回包含了原函数和新函数的‘代理'函数
beforefn.apply(this, arguments); //先执行新函数,且保证this不会被劫持,新函数接受的参数也会原封不动的传入原函数,新函数在原函数之前执行
return _self.apply(this, arguments); //再执行原函数并返回原函数的执行结果,并保证this不被劫持
}
}
Function.prototype.after = function(afterfn){
var _self = this; //保存原函数的引用
return function(){ //返回包含了原函数和新函数的‘代理'函数
var ret = _self.apply(this, arguments); //先执行原函数并返回原函数的执行结果,并保证this不被劫持,原函数执行的结果会赋值给ret变量,交由'代理'函数最后return
afterfn.apply(this, arguments); //再执行新函数,且保证this不会被劫持,新函数接受的参数也会原封不动的传入原函数,新函数在原函数之前执行
return ret;
}
}
//定义原函数
var print = function(){
console.log('打印原函数执行结果');
}
print = print.before(function(){
console.log('打印前置装饰函数的执行结果');
})
print = print.after(function(){
console.log('打印后置装饰函数的执行结果');
})
//执行装饰后的print函数,为原函数print函数添加的装饰器对用户来说看起来是透明的
print();
//打印结果
/*
打印前置装饰函数的执行结果
打印原函数执行结果
打印后置装饰函数的执行结果
*/
上述AOP装饰器通过在Function.prototype上直接添加before和after方法实现。然而,直接污染函数原型在某些场景下并非最佳实践。一种变通方案是将原函数和新函数均作为参数传入before和after方法中,从而避免原型污染。
非原型污染的实现变通
var before = function(fn, beforefn){
return function(){
beforefn.apply(this, arguments)
return fn.apply(this, arguments)
}
}
var after = function(fn, agterfn){
return function(){
var ret = fn.apply(this, arguments)
agterfn.apply(this, arguments)
return ret;
}
}
var a = function(){
console.log('原函数执行结果');
}
a = before(a, function(){
console.log('前置装饰函数执行结果');
})
a = after(a, function(){
console.log('后置装饰函数执行结果');
})
a()
/*
前置装饰函数执行结果
原函数执行结果
后置装饰函数执行结果
*/
最后,通过一个具体的装饰器模式应用实例来巩固理解。
在实际开发中,用户数据上报是一个常见需求。通常在项目开发后期,此类需求才会陆续提出。若此时直接在对应业务函数中嵌入数据上报代码,不仅修改繁琐,还会增加开发与测试成本。此时,利用装饰器模式将上报函数装饰到原有业务函数上,是更为理想的解决方案。
装饰器模式在数据上报中的应用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>装饰者模式应用数据上报</title>
</head>
<body>
<button type="button" id="btn">点击登录并上报数据</button>
</body>
<script>
var showDialog = function(){
console.log('显示登录弹窗');
}
var log = function(){
console.log('计数上报');
}
var after = function(fn, afterFn){
return function(){
var ret = fn.apply(this, arguments)
afterFn.apply(this, arguments)
return ret;
}
}
showDialog = after(showDialog, log)
document.getElementById('btn').onclick = showDialog;
</script>
</html>
以上内容即为本文的全部讲解,希望能为读者的学习提供有益帮助,也欢迎大家继续支持本站。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
