JavaScript装饰器实现原理与实战解析

作者:闭包旅人

时间:2026-10-25

浏览:284

本文深入解析JavaScript中装饰器的实现原理,涵盖类属性与类的装饰机制。通过ES6语法糖与Object.defineProperty的关系,阐明装饰器的工作流程,并结合TypeScript中的实际案例,展示如何为类及其方法添加功能扩展,如属性只读控制、类增强及方法叠加等应用场景。

文章目录

近期在采用TS结合Vue的开发架构时,观察到项目中广泛使用了装饰器特性,这引发了我对底层实现机制的探究兴趣。为了透彻理解这一知识点,特此梳理其核心原理。

装饰器的常见应用场景

  • 对类属性进行装饰处理
  • 对类本身进行装饰处理

需要明确的是,装饰器仅能作用于类及其属性,无法直接应用于普通函数,这主要归因于JavaScript中存在的函数提升机制。

接下来,我们将通过具体示例分别阐述这两种装饰情况。

类属性装饰详解

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}
class Cat {
    @readonly
    say() {
        console.log("meow ~");
    }
}
var kitty = new Cat();
kitty.say = function() {
    console.log("woof !");
}
kitty.say()    // meow ~

在ES6规范中,类的定义本质上是一种语法糖,其底层实现仍依赖于构造函数。类属性的定义过程实际调用了 Object.defineProperty() 方法。上述类的代码结构可以等价转换为以下形式:

class Cat {
    say() {
        console.log("meow ~");
    }
}

// 上面等价于下面代码

function Cat() {}

Object.defineProperty(Cat.prototype, "say", {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
});

当在 say 方法前添加 @readonly 装饰器后,整个代码逻辑等价于执行以下操作:

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}

function Cat() {}

let descriptor = {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
};

descriptor = readonly(Cat.prototype, 'say', descriptor) || descriptor;

Object.defineProperty(Cat.prototype, "say", descriptor);

类属性装饰器的底层实现原理,正是基于上述语法糖的转换过程。

此处需特别注意装饰器的书写规范:应在属性前使用 @readonly 形式,而非 @readonly() 形式。若采用后者写法,装饰器函数必须返回一个函数以表示装饰行为。这种传参写法的主要目的是通过参数扩展装饰器功能,从而提升装饰器函数的复用性。

例如,可以通过传参来控制是否设置属性为只读,具体代码如下:

function readonly(flag) {
    return function (target, name, descriptor) {
        if (flag) {
            discriptor.writable = false;
        } else {
            discriptor.writable = true;
        }
        return discriptor;
    }
}
class Cat {
    @readonly(true)
    say() {
        console.log("meow ~");
    }
}
var kitty = new Cat();
kitty.say = function() {
    console.log("woof !");
}
kitty.say()

类装饰详解

在装饰一个类时,类本身在本质上是一个函数,并不具备描述符(descriptor)。此时,装饰器接收的 target 参数即为该函数本身。

function isAnimal(target) {
    target.isAnimal = true;
  	return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true

换言之,上述 @isAnimal 装饰器的作用等同于执行以下代码:

Cat = isAnimal(function Cat() { ... });

关键注意点

作用于具体方法上的 decorator 接收的第一个参数(target)指向类的 prototype;而若将一个 decorator 直接作用于类本身,则其第一个参数 target 指向的是类构造函数本身。

装饰器的执行时机发生在属性定义阶段,这意味着被装饰的属性在定义完成后,即已变为经过装饰器处理后的新属性。

以下示例展示了装饰器的具体执行时序:

function log(message) {
    return function() {
        console.log(message);
    }
}
console.log('before class');
@log('class Bar')
class Bar {
    @log('class method bar');
    bar() {}
    @log('class getter alice');
    get alice() {}
    @log('class property bob');
    bob = 1;
}
console.log('after class');
let bar = {
    @log('object method bar')
    bar() {}
};

控制台输出结果如下:

before class class method bar class getter alice class property bob class Bar after class object method bar

从输出结果可以看出,装饰器在类及其属性定义阶段便已介入并完成了“装饰”操作。

实际应用场景

本示例设定的场景如下:

  • 首先,创建一个基础的 Man 类,设定其防御值为 2,攻击力为 3,血量为 3;
  • 接着,为其装备钢铁侠盔甲,使防御力增加 100,总计变为 102;
  • 随后,为其配备光束手套,使攻击力增加 50,总计变为 53;
  • 最后,为其增加“飞行”能力。

1. 构建 Man 类:

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`); 

// 输出:当前状态 ===> 防御力:2,攻击力:3,血量:3

2. 利用装饰器为钢铁侠装配盔甲:

function decorateArmour(target, key, descriptor) {
  const method = descriptor.value;
  let moreDef = 100;
  let ret;
  descriptor.value = (...args)=>{
    args[0] += moreDef;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3

3. 利用装饰器增加光束手套功能:

function decorateLight(target, key, descriptor) {
  const method = descriptor.value;
  let moreAtk = 50;
  let ret;
  descriptor.value = (...args)=>{
    args[1] += moreAtk;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  @decorateLight
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
...
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
//输出:当前状态 ===> 防御力:102,攻击力:53,血量:3

在此处可以清晰看到装饰模式的优势:它支持对特定方法进行叠加使用,对原类的侵入性极小,仅需增加一行 @decorateLight 即可实现,便于灵活增删功能;同时具备良好的代码复用性。

4. 增加飞行能力:

function addFly(canFly){
  return function(target){
    target.prototype.canFly = canFly;
    let extra = canFly ? '(技能加成:飞行能力)' : '';
    let method = target.prototype.toString;
    target.prototype.toString = (...args)=>{
      return method.apply(target.prototype,args) + extra;
    }
    return target;
  }
}

@addFly(true)
class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  @decorateLight
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  ...
}
...

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:53,血量:3(技能加成:飞行能力)

至此,关于JavaScript装饰器实现原理的详细介绍已告一段落。如需了解更多JavaScript装饰器相关内容,欢迎查阅本站过往文章或继续浏览后续相关文章,感谢大家的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章