JS如何实现装饰器?装饰器模式应用

在javascript中实现装饰器主要有两种方式:一是使用高阶函数,二是采用es7+的装饰器语法(@decorator)。高阶函数通过接收原函数并返回增强后的新函数,可在不修改原函数的前提下添加日志、缓存、性能监控等横切功能,兼容性好且无需转译,适用于函数级别的装饰;而es7+装饰器语法更具声明性,支持类和类成员的装饰,通过@符号直接标注,可实现类的密封、方法只读、废弃提示、权限校验、数据验证、依赖注入和路由定义等高级元编程能力,但需借助babel等工具转译,目前仍处于提案阶段。两种方式均遵循开闭原则,实现关注点分离、代码复用与可维护性提升,其中高阶函数适用于通用函数增强,es7+装饰器更适合类结构化应用与框架开发,开发者可根据项目需求和技术栈选择合适方案。

JS如何实现装饰器?装饰器模式应用

在JavaScript中实现装饰器,主要有两种途径:一种是利用高阶函数(Higher-Order Functions)来包装现有函数或类,这是装饰器模式在JS中的经典实现;另一种是借助ES7+提案中的装饰器语法(

@decorator

),这种语法更具声明性,但目前仍处于提案阶段,需要Babel等工具转译才能使用。核心思想都是在不修改原有代码结构的前提下,为对象或函数添加新的行为或修改其现有行为。

解决方案

在JavaScript中,实现装饰器模式,我们通常会选择以下两种方式,这取决于你当前项目的技术栈和对未来语言特性的采纳程度。

1. 基于高阶函数的实现(函数装饰器)

这是最通用、兼容性最好的方式。一个高阶函数接收一个函数作为参数,并返回一个新的函数,这个新函数在执行时会包含原函数的功能,并附加了额外的逻辑。

// 简单的日志装饰器function logDecorator(func) {  return function(...args) {    console.log(`Calling function: ${func.name || 'anonymous'} with args:`, args);    const result = func.apply(this, args); // 保持this上下文    console.log(`Function ${func.name || 'anonymous'} returned:`, result);    return result;  };}// 简单的缓存装饰器function memoizeDecorator(func) {  const cache = {};  return function(...args) {    const key = JSON.stringify(args); // 简单的缓存键生成    if (cache[key]) {      console.log(`Cache hit for ${func.name || 'anonymous'} with args:`, args);      return cache[key];    }    console.log(`Cache miss for ${func.name || 'anonymous'} with args:`, args);    const result = func.apply(this, args);    cache[key] = result;    return result;  };}// 示例应用function add(a, b) {  return a + b;}const loggedAdd = logDecorator(add);loggedAdd(1, 2); // 会输出日志const memoizedAdd = memoizeDecorator(add);memoizedAdd(3, 4); // 第一次计算并缓存memoizedAdd(3, 4); // 第二次直接从缓存获取

2. 基于ES7+提案的装饰器语法(类装饰器与方法装饰器)

这种方式更接近其他语言(如Python、Java)的装饰器语法,通过

@

符号直接应用于类或类的方法上。这极大提升了代码的可读性和声明性,但需要Babel等工具进行转译。

类装饰器:

一个类装饰器接收构造函数作为参数,并可以返回一个新的构造函数,或者修改原构造函数。

// @sealed 装饰器:让类不可扩展,属性不可配置function sealed(constructor) {  Object.seal(constructor);  Object.seal(constructor.prototype);  return constructor; // 可以选择返回原构造函数或新的构造函数}// @logClass 装饰器:打印类被创建时的信息function logClass(constructor) {  console.log(`Class ${constructor.name} was defined.`);  return class extends constructor {    constructor(...args) {      super(...args);      console.log(`Instance of ${constructor.name} created.`);    }  };}@sealed@logClass // 多个装饰器会从下往上执行,但实际应用时,类装饰器通常是修改或替换类本身class MyClass {  constructor(name) {    this.name = name;  }  greet() {    return `Hello, ${this.name}!`;  }}const instance = new MyClass('World');console.log(instance.greet());// 尝试修改或扩展 MyClass 会失败try {  MyClass.prototype.newMethod = function() {};} catch (e) {  console.error("Cannot add new method to sealed class prototype:", e.message);}

方法装饰器:

方法装饰器接收三个参数:

target

(类的原型对象)、

key

(方法名)、

descriptor

(属性描述符)。它通常通过修改

descriptor.value

来改变方法的行为。

// @readonly 装饰器:让方法变为只读(不可修改、不可删除)function readonly(target, key, descriptor) {  descriptor.writable = false;  return descriptor;}// @deprecate 装饰器:标记方法已废弃function deprecate(message) {  return function(target, key, descriptor) {    const originalMethod = descriptor.value;    descriptor.value = function(...args) {      console.warn(`Warning: Method "${key}" is deprecated. ${message}`);      return originalMethod.apply(this, args);    };    return descriptor;  };}class User {  constructor(name) {    this.name = name;  }  @readonly  getName() {    return this.name;  }  @deprecate('Please use getName() instead.')  oldGetName() {    return this.name;  }}const user = new User('Alice');console.log(user.getName());user.oldGetName(); // 会触发废弃警告// 尝试修改 getName 方法会失败try {  user.getName = function() { return 'New Name'; };} catch (e) {  console.error("Cannot reassign readonly method:", e.message);}

为什么我们需要装饰器模式?

思考一下,我们写代码时总会遇到一些横切关注点(Cross-cutting Concerns),比如日志记录、性能监控、权限校验、数据验证、事务管理等等。这些功能往往散落在应用程序的各个模块中,如果直接把它们的代码写到业务逻辑里,那业务代码就会变得臃肿、难以维护。

装饰器模式,本质上就是提供了一种优雅的方案来处理这些横切关注点。它允许你在不修改原有对象(或函数)代码的前提下,动态地给它们“穿上”一层新的功能外衣。这就像给一个普通杯子加上保温套、防滑垫一样,杯子本身还是那个杯子,但它现在有了额外的功能。

这种模式的好处显而易见:

分离关注点: 业务逻辑保持纯粹,与非业务功能解耦。代码复用: 装饰器本身是可复用的功能模块,可以应用到多个不同的目标上。可维护性: 当需要修改或移除某个横切功能时,只需修改或移除对应的装饰器,而无需触碰核心业务逻辑。开闭原则: 对扩展开放,对修改封闭。你可以很方便地添加新功能(通过新增装饰器),而无需修改已有的代码。

我个人觉得,当你发现自己的函数或类开始变得“胖”起来,里面塞满了各种与核心职责无关的辅助性代码时,就应该考虑引入装饰器了。它能让你的代码看起来更整洁,也更符合面向对象的设计原则。

如何使用高阶函数实现JavaScript装饰器?

高阶函数实现装饰器,其实是JavaScript函数式编程思想的一种体现。它非常灵活,不需要任何特殊的语法支持,因此在任何JS环境中都可以使用。

核心原理很简单:一个函数(我们称之为“装饰器函数”)接收另一个函数作为参数,然后返回一个新的函数。这个新返回的函数在内部调用原始函数,并在其前后或者特定位置插入额外的逻辑。

举个例子,假设我们有一个计算阶乘的函数:

function factorial(n) {  if (n === 0 || n === 1) {    return 1;  }  return n * factorial(n - 1);}

现在,我们想知道这个函数每次执行花了多少时间。我们不希望直接修改

factorial

函数,因为那样会污染它的核心逻辑。这时,一个高阶函数就能派上用场:

// 性能监控装饰器function measurePerformance(originalFunc) {  return function(...args) { // 返回一个新的函数    const start = performance.now(); // 记录开始时间    const result = originalFunc.apply(this, args); // 调用原始函数,并保持this上下文    const end = performance.now(); // 记录结束时间    console.log(`${originalFunc.name || 'anonymous'} took ${end - start} ms to execute.`);    return result; // 返回原始函数的执行结果  };}// 应用装饰器const measuredFactorial = measurePerformance(factorial);// 调用被装饰的函数console.log(measuredFactorial(5));console.log(measuredFactorial(10));

这里的

measurePerformance

就是一个高阶函数实现的装饰器。它接收

factorial

作为参数,返回了一个新的函数

measuredFactorial

。当我们调用

measuredFactorial

时,实际上是执行了

measurePerformance

内部返回的那个匿名函数,这个匿名函数在调用

factorial

的前后添加了计时逻辑。

这种方式的优点在于它的纯粹性通用性。你可以很方便地组合多个高阶函数装饰器,形成一个功能链。比如,你可以在

measurePerformance

之前再套一个

logDecorator

,先打印日志再测量性能。

当然,也有一些小“缺点”:它不如ES7+的

@

语法直观,每次都需要手动包装。另外,对于类的方法,你需要手动访问原型链上的方法进行包装,不如ES7+方法装饰器那样直接作用于方法定义上。但就函数而言,高阶函数装饰器绝对是首选。

ES7+的装饰器语法有哪些特点与应用场景?

ES7+(目前仍是Stage 3提案)引入的装饰器语法,用一个

@

符号,让装饰器的应用变得异常简洁和声明性。它主要针对类的成员(方法、属性、Getter/Setter)进行装饰,而不是像高阶函数那样直接装饰普通的函数。这让它在构建大型、结构化的应用程序时,特别是与框架(如Angular、NestJS)结合时,显得特别强大。

特点:

声明性语法: 直接在类或方法定义上方使用

@decoratorName

,一目了然地表明该类或方法被赋予了额外功能。作用目标明确: 装饰器函数会接收到关于被装饰目标(类、方法等)的特定信息,例如类构造函数、方法名、属性描述符等。这使得装饰器可以精确地修改或增强目标。可组合性: 多个装饰器可以堆叠在同一个目标上,它们会按照从下往上的顺序执行(对于类和方法装饰器而言,执行顺序是自底向上,但通常会认为最靠近目标的装饰器先应用)。元编程能力: 装饰器在运行时提供了修改类定义或方法行为的能力,这是一种强大的元编程(meta-programming)工具。

应用场景:

ES7+装饰器在实际开发中有着广泛的应用,尤其是在框架和库的开发中,它们能极大地简化代码并提升可读性。

日志记录与性能监控: 这是最常见的应用。你可以创建一个

@log

装饰器,自动记录方法被调用时的参数和返回值;或者一个

@measure

装饰器,统计方法的执行时间。

// 假设这是我们的装饰器实现// function log(target, key, descriptor) { /* ... */ }// function measure(target, key, descriptor) { /* ... */ }class Calculator {  @log  @measure  add(a, b) {    return a + b;  }}const calc = new Calculator();calc.add(1, 2); // 自动打印日志和性能数据

权限控制与认证: 在Web应用中,经常需要检查用户是否有权限访问某个方法或资源。

@authRequired

@hasRole('admin')

这样的装饰器可以非常方便地在方法执行前进行权限校验。

// function authRequired(target, key, descriptor) { /* ... */ }class AdminPanel {  @authRequired  deleteUser(userId) {    console.log(`Deleting user: ${userId}`);  }}

数据验证: 在处理表单输入或API请求时,往往需要对数据进行严格的验证。

@validate(schema)

装饰器可以在方法执行前自动根据预定义的Schema进行数据校验,如果失败则抛出错误。

// function validate(schema) { return function(target, key, descriptor) { /* ... */ }; }class UserService {  @validate({ name: String, age: Number })  createUser(data) {    console.log('Creating user:', data);  }}

依赖注入: 某些框架(如Angular)使用装饰器来声明类所需的依赖,框架在创建实例时会自动注入这些依赖。例如

@Injectable()

@Inject(TOKEN)

路由定义: 在一些后端框架(如NestJS)中,装饰器被用来定义HTTP请求的路由和方法。

@Get('/users')

@Post()

等。

状态管理: 在某些UI库或框架中,装饰器可以用来标记可观察对象或响应式属性,以便在它们改变时自动更新UI。

ES7+装饰器为JavaScript带来了更强的表现力和更清晰的代码结构,尤其适合那些需要大量元编程和横切关注点处理的复杂应用。然而,由于它仍是提案,生产环境中使用需要注意转译工具的配置和潜在的未来语法变动。

以上就是JS如何实现装饰器?装饰器模式应用的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/113456.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月24日 03:45:06
下一篇 2025年11月24日 04:16:43

相关推荐

  • Go语言中实现字节序处理:encoding/binary包解析无符号16位整数

    本文深入探讨go语言中如何使用`encoding/binary`包高效处理字节数据,特别是针对无符号16位整数的读写操作。通过`bigendian`和`littleendian`接口,可以轻松实现类似node.js `buffer`的`readuint16be`功能,确保跨平台数据交换的字节序兼容性…

    2025年12月16日
    000
  • Golang如何在多操作系统中统一开发环境_Golang跨系统环境搭建全攻略

    统一Golang多系统开发环境需标准化版本、模块、构建与编辑器配置。1. 使用gvm或asdf统一Go版本,Windows可用choco安装;2. 启用Go Modules并设置GO111MODULE=on,提交go.mod与go.sum;3. 利用GOOS/GOARCH交叉编译,结合Makefil…

    2025年12月16日
    000
  • 深入解析Go语言高并发场景下的defer与内存管理

    本文探讨了Go语言在高并发UDP日志处理场景中,由于`defer`闭包导致的内存急剧增长问题。通过`pprof`工具定位到`newdefer`和`runtime.deferproc`是内存消耗的主要来源。文章分析了该问题曾是Go语言运行时的一个已知bug,并提供了解决方案:升级Go版本以修复底层bu…

    2025年12月16日
    000
  • 如何在Golang中处理HTTP POST请求

    答案:Go通过net/http库处理HTTP POST请求。服务端使用http.HandleFunc解析表单或JSON数据,客户端用http.Post发送数据,需注意方法校验、Content-Type检查、请求体读取与响应关闭。 在Golang中处理HTTP POST请求,主要依赖标准库net/ht…

    2025年12月16日
    000
  • 如何查看Go二进制文件编译时使用的Go版本

    本文介绍了一种简单有效的方法,用于识别go二进制文件在编译时所使用的go版本。通过结合使用`strings`和`grep`命令,开发者可以快速检查任何go可执行文件的构建环境信息,这对于管理多个go版本或验证构建一致性至关重要。 核心方法:识别Go二进制文件的构建版本 在Go语言的开发和部署实践中,…

    2025年12月16日
    000
  • 提升SQL INSERT语句可读性:SET语法详解

    当sql insert语句涉及大量列时,传统的values列表可能导致可读性下降,难以快速识别值与列的对应关系。本文将介绍mysql中insert … set语法,它通过直接将列名与值进行关联,显著提升了语句的清晰度和可维护性,特别适用于复杂的插入操作。 在数据库操作中,INSERT语句…

    2025年12月16日
    000
  • 深入解析Go语言中newdefer引发的内存爆炸问题及解决方案

    本文深入探讨go语言应用中由`newdefer`导致的内存爆炸问题,特别是在高并发、大量使用`defer`结合`panic`/`recover`模式的场景下。通过分析`pprof`报告,揭示了该问题可能源于go运行时特定版本中的bug。教程强调了通过升级go版本来解决已知运行时缺陷的重要性,并提供了…

    2025年12月16日
    000
  • 解决 Go 模板执行中的 “i/o timeout” 错误

    本文旨在帮助开发者解决 Go 模板执行过程中遇到的 “i/o timeout” 错误。该错误通常发生在模板引擎尝试将渲染结果写入 HTTP 响应时,由于写入超时导致。文章将分析错误原因,并提供解决方案,包括检查 `http.Server` 的 `WriteTimeout` 设…

    2025年12月16日
    000
  • Go语言中实现类似Node.js readUInt16BE的字节序操作

    本文将指导您如何在go语言中实现类似node.js buffer.readuint16be的功能。通过使用go标准库中的encoding/binary包,您可以高效地处理字节序(大端序和小端序)的16位无符号整数的读写操作,确保跨平台数据交换的准确性与兼容性。 在进行跨系统或网络通信时,处理多字节数…

    2025年12月16日
    000
  • 如何查看Go二进制文件构建时使用的Go版本

    本文介绍一种识别go语言编译的二进制文件所使用go版本的实用方法。通过结合使用`strings`和`grep`命令,开发者可以快速提取二进制文件中嵌入的go版本信息。这对于管理多个go环境、验证构建一致性或排查版本相关问题具有重要意义,提供了一种简洁高效的解决方案。 在Go语言开发中,尤其是在拥有多…

    2025年12月16日
    000
  • Go语言:健壮的命令行参数解析与用户空白输入检测

    本教程旨在指导go语言开发者如何有效地处理命令行参数和用户输入。我们将探讨如何校验命令行参数的数量,实现默认值设置,并介绍如何准确识别用户输入的空行(回车键),避免`fmt.scanf`在处理空白输入时的常见陷阱,从而提升应用程序的健壮性和用户体验。 在开发命令行工具或交互式应用程序时,正确地解析用…

    2025年12月16日
    000
  • 规范化Go程序错误处理与优雅退出机制

    本文探讨了go语言中处理程序错误并以非零状态码退出的规范方法。针对os.exit和log.fatal跳过defer函数的问题,推荐采用将核心逻辑封装在返回error的run函数中,并在main函数中处理其返回错误的模式,以确保defer函数得以执行,实现程序的优雅终止。 Go程序错误处理与优雅退出 …

    2025年12月16日
    000
  • 如何在Golang项目中实现CI/CD流水线

    编写Go测试用例确保代码质量,使用go test运行单元测试并生成覆盖率报告;2. 选择GitHub Actions等CI/CD平台配置自动化流水线,触发测试与构建;3. 可选Docker镜像构建,通过Dockerfile多阶段构建减小体积并推送到镜像仓库;4. 部署阶段通过SSH、Kubernet…

    2025年12月16日
    000
  • Go 应用程序的错误退出:兼顾 deferred 函数执行

    在 go 语言中,直接使用 `os.exit` 或 `log.fatal` 会立即终止程序,跳过已注册的延迟函数。本文将探讨 go 程序中带错误码退出的最佳实践,介绍一种将主要逻辑封装在 `run` 函数中的模式,该模式能确保错误得到妥善处理,并在退出前允许所有延迟函数正常执行,从而实现更健壮和可维…

    2025年12月16日
    000
  • Golang如何使用Etcd实现服务发现与注册_Golang Etcd服务发现注册实践详解

    使用Golang结合Etcd实现服务注册与发现,通过租约机制维持心跳、Watch监听服务变化。服务启动时将信息写入Etcd并创建租约,定时续租保持活跃;客户端通过前缀查询实例并监听变更,实现动态服务发现。生产环境中需封装模块、添加重试、合理设置TTL,并结合健康检查提升稳定性。该方案已广泛应用于Ku…

    2025年12月16日
    000
  • Golang如何使用观察者模式实现事件订阅_Golang观察者模式事件订阅实践详解

    观察者模式通过接口与结构体实现事件订阅,主题维护观察者列表并通知状态变更,如用户注册时触发邮件发送与资料初始化。 在Go语言中,观察者模式是一种常见的设计模式,用于实现对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。这种机制非常适合实现事件订阅系统。下面通过一个清晰的结构…

    2025年12月16日
    000
  • Golang如何在API接口中返回标准错误

    定义标准错误结构并封装响应函数,使用预定义错误码与HTTP状态码返回统一格式的错误信息,避免暴露敏感细节。 在Go语言开发API接口时,返回标准错误能让客户端更清晰地理解出错原因。直接返回原始错误信息不仅不友好,还可能暴露敏感信息。正确的做法是统一错误格式,结合HTTP状态码,提供可读性强、结构一致…

    2025年12月16日
    000
  • Go语言JSON解码器处理私有字段:深入解析与两种解决方案

    本教程详细探讨go语言`encoding/json`包在解码json数据到结构体私有字段时遇到的常见问题。文章提供了两种核心解决方案:通过导出结构体字段并结合json标签进行映射,或实现自定义`json.unmarshaler`接口以实现更精细的控制,确保json数据能够正确地反序列化到go结构体中…

    2025年12月16日
    000
  • 如何在Golang中实现Web请求限流_Golang Web请求限流方法汇总

    答案:Golang中常用限流方式包括令牌桶算法、基于IP的限流、中间件封装及分布式限流。1. 使用golang.org/x/time/rate实现令牌桶限流,控制请求速率;2. 结合sync.RWMutex或sync.Map对不同IP独立限流;3. 将限流逻辑封装为HTTP中间件提升复用性;4. 多…

    2025年12月16日
    000
  • Go语言中实现泛型Map键类型转换以支持JSON序列化

    本文旨在解决go语言中将`map[int]t`类型转换为`map[string]t`以进行json序列化时,因类型t不同而需要编写大量重复转换函数的问题。我们将探讨如何利用go的反射机制,实现一个通用的函数,能够将任意`map[int]t`(或其他整型键)转换为`map[string]interfa…

    2025年12月16日
    000

发表回复

登录后才能评论
关注微信