终于等到原生装饰器:用JavaScript新语法为你的类增加超能力

从 TypeScript 的 @Component 到 Angular 的 @Injectable,装饰器已经是前端开发中几乎绕不开的语法。不过很长一段时间里,它只是 TypeScript 的”私货”和 Babel 插件的模拟产物。直到 2025 年,随着 ES2025 标准的推进和 Chrome 126 的正式支持,我们终于可以在原生 JavaScript 里使用装饰器了。它不需要编译,不需要类型注解,写出来就是标准的 ECMAScript。这篇文章会带你从最基础的语法开始,一步步用三个真实场景的装饰器把它的能力全部摸清。

一、没有装饰器之前,我们怎么给类”加料”

假设你有一个 UserService 类,里面有个 updateProfile 方法。你想在每次调用这个方法时自动记录一条日志,同时把调用参数和时间打到控制台。传统的做法是:手动在方法体开头加 console.log,或者写一个高阶函数去包裹原方法。但逻辑一多,方法体重叠了大量的旁路代码,跟核心业务毫无关系。

另一种“优雅”点的方案是:写一个工厂函数,接收原类或原方法,返回增强后的版本。但这样搞几次之后,调用方式就变了,原本是 new UserService(),现在得 createLoggedService(UserService),而且每次想加缓存、加权限检查就得不断往里套娃,代码的结构跟原始意图差得越来越远。

装饰器解决的正是这个问题:把增强逻辑抽离成函数,以声明的方式标注在类的成员上。你看着类的主体,眼里净是业务代码,那些辅助逻辑被 @ 符号带到了类的顶部或方法的上面,一目了然。

二、原生装饰器的基本语法规则

先明确一个要点:当前的 JavaScript 原生装饰器只能用在类成员上——也就是类字段、方法、getter 和 setter。类本身还没有支持类装饰器(那是后续提案的事)。

一个装饰器本质上就是一个普通函数,只不过它接收两个参数:

  • target:如果是静态成员,就是类的构造函数;如果是实例成员,就是 Class.prototype
  • context:一个提供元数据的对象,包含成员名称、种类(field/method/getter/setter)、是否静态等信息,还有一个重要的 addInitializer 方法,允许你在类实例化或类定义完成时插入一段初始化逻辑。

下面这个最简单的例子,@log 装饰器会在方法被调用时自动打印方法名和参数:

function log(target, context) {
    const methodName = String(context.name);
    // 获取原始方法
    const original = target;

    // 返回一个新的函数替换原方法
    function replacement(...args) {
        console.log(`调用 ${methodName},参数:`, args);
        const result = original.call(this, ...args);
        console.log(`${methodName} 执行完毕`);
        return result;
    }
    return replacement;
}

class Calculator {
    @log
    add(a, b) {
        return a + b;
    }
}

const calc = new Calculator();
calc.add(3, 5);
// 控制台输出:
// 调用 add,参数: [3, 5]
// add 执行完毕

过程很清晰:add 方法被装饰器替换成了 replacement,但原本的计算逻辑依旧在其中调用。同时,因为装饰器返回的是一个新函数,原型链上的方法引用会被自动修正,calc.add === Calculator.prototype.add 依然是 true。

三、实战一:带参数的装饰器——制作一个通用的计时器

日志装饰器虽然好用,但不能定制,比如我们想给不同的方法打上不同的标签,而不是统一用方法名。这时候就需要“装饰器工厂”——一个返回装饰器的高阶函数。

下面的 timer 装饰器工厂接收一个标签字符串,返回一个真正的装饰器函数,在方法执行前后输出耗时:

function timer(label) {
    return function(target, context) {
        const original = target;
        function replacement(...args) {
            const start = performance.now();
            const result = original.call(this, ...args);
            const duration = (performance.now() - start).toFixed(2);
            console.log(`${label} 耗时: ${duration}ms`);
            return result;
        }
        return replacement;
    };
}

class DataFetcher {
    @timer('数据加载')
    fetchLargeDataset() {
        // 模拟一个耗时操作
        const start = Date.now();
        while (Date.now() - start < 200) {}
        return { items: [] };
    }

    @timer('计算统计')
    computeStats(data) {
        const start = Date.now();
        while (Date.now() - start < 50) {}
        return { mean: 0, std: 0 };
    }
}

const fetcher = new DataFetcher();
fetcher.fetchLargeDataset();  // 输出: 数据加载 耗时: 201.xxms
fetcher.computeStats();      // 输出: 计算统计 耗时: 51.xxms

这种工厂模式让你可以在装饰器上传递任何配置信息——标签、阈值、开关等——与 TypeScript 装饰器的常见用法完全一致。

四、实战二:基于 Map 的内存缓存装饰器

很多计算结果是有幂等性的,拿相同的参数调用就应该返回相同的结果,比如根据用户 ID 查询数据库。我们可以写一个 @memoize 装饰器,把方法的结果按参数缓存起来,下次调用时直接返回缓存值,避免重复计算或请求。

function memoize(target, context) {
    const original = target;
    const cache = new Map();

    function replacement(...args) {
        // 用 JSON.stringify 生成简单的缓存键(适用于基本类型参数)
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log(`从缓存返回 ${String(context.name)}`);
            return cache.get(key);
        }
        const result = original.call(this, ...args);
        cache.set(key, result);
        return result;
    }
    return replacement;
}

class UserRepository {
    @memoize
    getUserById(id) {
        console.log(`查询数据库: id=${id}`);
        return { id, name: `用户${id}` };
    }
}

const repo = new UserRepository();
const user1 = repo.getUserById(101); // 查询数据库: id=101
const user2 = repo.getUserById(101); // 从缓存返回 getUserById
console.log(user1 === user2);        // true

这个装饰器内部创建了一个独立的 Map,缓存跟着方法走,不污染类实例。如果你的方法是处理大量重复请求(比如列表页的搜索建议),这种方式能直接省掉一大截接口调用。

五、实战三:权限控制装饰器——在方法执行前检查用户状态

装饰器最拿手的一个场景就是前置拦截。比如一个功能只对管理员开放,如果用户角色不对,直接拒绝调用,甚至都不需要进入方法体。我们可以写一个 @requireRole 装饰器:

function requireRole(role) {
    return function(target, context) {
        const original = target;
        function replacement(...args) {
            // 假设 this 上有一个 currentUser 属性
            if (!this.currentUser || this.currentUser.role !== role) {
                throw new Error(`权限不足:需要 ${role} 角色`);
            }
            return original.call(this, ...args);
        }
        return replacement;
    };
}

class AdminPanel {
    // 实例属性,模拟当前用户
    currentUser = null;

    @requireRole('admin')
    deletePost(postId) {
        console.log(`帖子 ${postId} 已删除`);
    }

    @requireRole('editor')
    editPost(postId, content) {
        console.log(`帖子 ${postId} 已更新`);
    }
}

const panel = new AdminPanel();
panel.currentUser = { name: '小李', role: 'editor' };

try {
    panel.editPost(42, '新内容');   // 帖子 42 已更新
    panel.deletePost(42);          // 抛出 Error: 权限不足:需要 admin 角色
} catch (e) {
    console.error(e.message);
}

这样,所有跟权限相关的逻辑只存在于装饰器中,类的方法内部专心处理业务。将来如果角色体系变动,只需要改装饰器,不需要在每一个方法里去搜 if (user.role !== 'admin')

六、使用 addInitializer 做初始化后的自动设置

装饰器的 context.addInitializer 是个很容易被忽略但非常实用的功能。它允许你在类实例创建之后、或者类定义完毕之后,自动运行一段代码。比如你想在每个对象创建时自动把某个属性设为只读,或者自动触发一次数据校验。

下面是一个简单的例子:让类实例在创建后自动输出一条初始化日志。

function autoLogInit(target, context) {
    context.addInitializer(function() {
        console.log(`实例已创建,成员 ${String(context.name)} 已就绪`);
    });
}

class Player {
    @autoLogInit
    health = 100;

    constructor(name) {
        this.name = name;
    }
}

const p1 = new Player('战士');
// 控制台输出: 实例已创建,成员 health 已就绪

如果给多个字段加了这个装饰器,每个都会触发自己的初始器。更复杂的场景里,你可以用它在类定义阶段收集所有带某装饰器的字段,生成一份“元数据映射”,后面用来做序列化、表单验证之类的功能——这就是很多框架里 @Prop 装饰器的基础原理。

七、原生装饰器与 TypeScript 装饰器的差异

如果你之前一直用的是 TypeScript 的实验性装饰器,切到原生装饰器时会有几个明显变化:

  • 原生装饰器目前不支持类装饰器(@classDecorator 写在 class 上方),那个能力还在提案阶段。你需要用其他方式模拟。
  • 参数装饰器(@param 写在方法参数前面)暂未支持,目前装饰器只能作用在类成员上。
  • 原生装饰器的 context 对象提供了更清晰的元数据入口,不像 TS 旧版那样依赖 Reflect.metadata。这对不依赖 TypeScript 的纯 JS 项目非常友好。
  • 原生装饰器是 ES 标准,不需要 experimentalDecorators 编译选项,可以直接在浏览器和 Node.js 里跑——前提是运行时版本够新。

八、运行环境与兼容性

截至 2025 年,装饰器在以下环境可直接使用,无需任何编译或 polyfill:

  • Chrome 126+、Edge 126+
  • Safari 18+(2024 年底加入)
  • Firefox 132+(2024 年 11 月已支持)
  • Node.js 22+(需要 v8 标志,但 Node 23 起已是默认特性)

如果你还在维护需要兼容旧浏览器的前端项目,可以继续使用 Babel 或 TypeScript 的装饰器编译方案,而服务端项目(Node.js 22 LTS 以上)完全可以放心用原生装饰器。

九、总结

装饰器最大的价值不是让你少写几行代码,而是把”增强逻辑”和”核心业务”清晰地分离开。你可以在不影响原始方法签名的情况下,给任何类方法加上缓存、日志、权限、节流、防抖这类横切关注点,而且这一切都是声明式的、可组合的。

文中的三个装饰器(日志、缓存、权限)可以直接拷贝到你的项目里使用。如果你还没试过原生装饰器,现在打开 Chrome 的控制台,粘贴第一个 @log 的例子,你会立刻感受到这种语法带来的清爽感——那个感觉就像第一次用 async/await 替代了回调地狱。

终于等到原生装饰器:用JavaScript新语法为你的类增加超能力
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript 终于等到原生装饰器:用JavaScript新语法为你的类增加超能力 https://www.taomawang.com/web/javascript/2384.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务