JS装饰者模式与TypeScript装饰器详解

作者:代码拾荒

时间:2026-10-24

浏览:595

本文深入解析JavaScript中的装饰者模式,阐述其通过包装链动态扩展对象职责的结构型特性。重点介绍TypeScript中装饰器的语法规范、配置方法及类、方法、属性等类型的装饰器用法。详细分析装饰器工厂的执行顺序,并通过TypeScript编译器生成的源码,揭示__decorate函数的内部实现机制与参数传递逻辑。

文章目录

装饰者模式介绍

装饰者模式(Decorator Pattern),亦称装饰器模式,是一种结构型设计模式。它允许在不修改原有对象代码的前提下,动态地为其添加新的职责或行为。

该模式的主要优势在于将对象的核心功能与附加的装饰功能解耦。这种非侵入式的修改方式,使得功能扩展更加灵活且不影响原有逻辑。

以图像处理为例,一个普通女孩可以通过美颜滤镜获得惊艳效果。通过叠加瘦脸、大眼、磨皮等装饰功能,最终呈现出完美的照片。

经过多重装饰后,人物的本质并未改变,但视觉效果得到了显著提升。若需不同风格,只需更换或组合不同的装饰功能即可,如同“百变星君”般灵活多变。

在代码层面,可以将每个装饰功能抽象为一个独立的类来实现。


// 女孩子
class Girl {
  faceValue() {
    console.log('我原本的脸')
  }
}

class ThinFace  {
  constructor(girl) {
    this.girl = girl;
  }
  faceValue() {
    this.girl.faceValue();
    console.log('开启瘦脸')
  }
}

class IncreasingEyes  {
  constructor(girl) {
    this.girl = girl;
  }
  faceValue() {
    this.girl.faceValue();
    console.log('增大眼睛')
  }
}

let girl = new Girl();
girl = new ThinFace(girl);
girl = new IncreasingEyes(girl);

// 闪瞎你的眼
girl.faceValue(); // 

从代码结构看,装饰者模式通过将一个对象嵌入另一个对象中,形成一条包装链。调用时,请求沿着链条传递,每个包装对象都有机会处理或增强该请求。

这种机制在增加或移除装饰功能时具有极高的灵活性。例如,若想展示真实面貌,只需移除“瘦脸”包装,其他功能不受影响;若需增加“磨皮”,只需添加对应功能类并继续装饰,各功能可并存运行。

在JavaScript中,使用类来实现装饰者模式可能显得较为笨重。鉴于JavaScript的灵活性,通常直接使用对象来实现装饰功能。


let girl = {
  faceValue() {
    console.log('我原本的脸')
  }
}
function thinFace() {
  console.log('开启瘦脸')
}
function IncreasingEyes() {
  console.log('增大眼睛')
}

girl.faceValue = function(){
  const originalFaveValue = girl.faceValue;  // 原来的功能
  return function() {
    originalFaveValue.call(girl);
    thinFace.call(girl);
  }
}()
girl.faceValue = function(){
  const originalFaveValue = girl.faceValue;  // 原来的功能
  return function() {
    originalFaveValue.call(girl);
    IncreasingEyes.call(girl);
  }
}()

girl.faceValue();

具体做法是在不修改原代码的基础上,先保存原始函数,然后重新定义该函数。在重写的函数内部调用之前保存的原始函数,从而实现功能的增强。

通过图示可以更直观地理解装饰者模式的原理。

装饰者模式介绍

从图中可见,通过层层包装,原有的对象功能被逐步增强,实现了功能的动态扩展。

TypeScript中的装饰器

TypeScript中的装饰器采用 @expression 语法,其中 expression 求值后必须是一个函数。该函数在运行时被调用,被装饰声明的元数据将作为参数传入。

目前,JavaScript规范中的装饰器提案尚处于第二阶段(Stage 2),这意味着原生浏览器环境暂不支持直接使用,需借助编译工具。

开发者可以使用Babel或TypeScript编译器,在编译阶段将装饰器语法转换为浏览器可执行的常规代码(后续将分析编译后的源码)。

本节主要讨论TypeScript中装饰器的具体使用方式。

TypeScript装饰器可应用于类声明、方法、访问器(getter/setter)、属性以及参数上。

启用装饰器支持,需在命令行编译时添加标志:


tsc --target ES5 --experimentalDecorators test.ts

或者在配置文件 tsconfig.json 中配置:


{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

装饰器的使用

装饰器本质上是一个函数,使用时需在其前添加 @ 符号,并置于被装饰声明之前。多个装饰器可同时作用于一个声明,可写在一行或换行书写:


// 换行写
@test1
@test2
declaration

//写一行
@test1 @test2 ...
declaration

定义 face.ts 文件:


function thinFace() {
  console.log('开启瘦脸')
}

@thinFace
class Girl {
}

编译生成的JS代码在运行时,会直接调用 thinFace 函数。由于该装饰器作用于类,因此被称为类装饰器。

若需附加多个功能,可组合使用多个装饰器:


function thinFace() {
  console.log('开启瘦脸')
}
function IncreasingEyes() {
  console.log('增大眼睛')
}

@thinFace
@IncreasingEyes
class Girl {
}

当多个装饰器组合使用时,需注意其调用顺序是从下至上依次执行,这与代码书写顺序相反。示例中的运行结果如下:

'增大眼睛'

'开启瘦脸'

若需在一个装饰器中为类添加属性,并在其他装饰器中使用,该属性定义应放在最后一个装饰器中,因为最后一个装饰器最先执行。

装饰器工厂

有时需要为装饰器传递参数,此时需借助装饰器工厂函数。工厂函数是一个高阶函数,调用后返回一个函数,返回的函数作为实际的装饰器函数。


function thinFace(value: string){
  console.log('1-瘦脸工厂方法')
  return function(){
    console.log(`4-我是瘦脸的装饰器,要瘦脸${value}`)
  }
}
function IncreasingEyes(value: string) {
  console.log('2-增大眼睛工厂方法')
  return function(){
    console.log(`3-我是增大眼睛的装饰器,要${value}`)
  }
}

@thinFace('50%')
@IncreasingEyes('增大一倍')
class Girl {
}

@ 符号后为工厂函数的调用,执行顺序从上到下,目的是获取最终的装饰器函数。装饰器函数的实际执行顺序依然遵循从下到上的规则。

运行结果如下:

1-瘦脸工厂方法

2-增大眼睛工厂方法

3-我是增大眼睛的装饰器,要增大一倍

4-我是瘦脸的装饰器,要瘦脸50%

总结执行流程:

  • 工厂函数从上到下依次执行,用于获取装饰器函数。
  • 装饰器函数的实际执行顺序是从下到上依次执行。

类装饰器

作用于类声明的装饰器,提供了修改类的机会。执行装饰器函数时,类的构造函数将作为参数传递给装饰器。


function classDecorator(value: string){
  return function(constructor){
    console.log('接收一个构造函数')
  }
}

function thinFace(constructor){
  constructor.prototype.thinFaceFeature = function() {
    console.log('瘦脸功能')
  }
}

@thinFace
@classDecorator('类装饰器')
class Girl {}

let g = new Girl();

g.thinFaceFeature(); // '瘦脸功能'

在上述示例中,获取构造函数后,可向原型添加新方法,甚至实现继承其他类的功能。

方法装饰器

作用于类的方法,包括静态方法和原型方法。作用于静态方法时,装饰器接收类构造函数;作用于原型方法时,装饰器接收原型对象。以下以原型方法为例。


function methodDecorator(value: string, Girl){
  return function(prototype, key, descriptor){
    console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
  }
}

function thinFace(prototype, key, descriptor){
  // 保留原来的方法逻辑
  let originalMethod = descriptor.value;
  // 改写,增加逻辑,并执行原有逻辑
  descriptor.value = function(){
    originalMethod.call(this);  // 注意修改this的指向
    console.log('开启瘦脸模式')
  }
}

class Girl {

  @thinFace
  @methodDecorator('方式装饰器', Girl)
  faceValue(){
    console.log('我是原本的面目')
  }
}

let g = new Girl();

g.faceValue();

从代码可见,装饰器函数接收三个参数:原型对象、方法名以及属性描述对象。若对描述对象不熟悉,可参考相关文档。

要增强功能,可先保留原函数,并将描述对象的 value 属性改写为新的函数。

当通过 g.faceValue() 访问方法时,实际访问的是描述对象 value 对应的值。

在改写的函数中增加逻辑,并执行之前保留的原函数。注意,原函数需使用 call 或 apply 将 this 指向原型对象。

属性装饰器

作用于类中定义的属性,这些属性并非原型上的属性,而是通过类实例化得到的实例对象上的属性。

装饰器同样接收两个参数:原型对象和属性名。为何没有属性描述对象?这与TypeScript初始化属性装饰器的机制有关。目前无法在定义原型成员时描述实例属性。


function propertyDecorator(value: string, Girl){
  return function(prototype, key){
    console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
  }
}

function thinFace(prototype, key){
  console.log(prototype, key)
}

class Girl {
  @thinFace
  @propertyDecorator('属性装饰器', Girl)
  public age: number = 18;
}

let g = new Girl();

console.log(g.age); // 18

其他装饰器的写法

以下组合多个装饰器书写,除上述三种外,还包括访问器装饰器和参数装饰器。这些装饰器同时存在时,有特定的执行顺序。


function classDecorator(value: string){
  console.log(value)
  return function(){}
}
function propertyDecorator(value: string) {
  console.log(value)
  return function(){
    console.log('propertyDecorator')
  }
}
function methodDecorator(value: string) {
  console.log(value)
  return function(){
    console.log('methodDecorator')
  }
}
function paramDecorator(value: string) {
  console.log(value)
  return function(){
    console.log('paramDecorator')
  }
}
function AccessDecorator(value: string) {
  console.log(value)
  return function(){
    console.log('AccessDecorator')
  }
}
function thinFace(){
  console.log('瘦脸')
}
function IncreasingEyes() {
  console.log('增大眼睛')
}


@thinFace
@classDecorator('类装饰器')
class Girl {
  @propertyDecorator('属性装饰器')
  age: number = 18;
  
  @AccessDecorator('访问符装饰器')
  get city(){}

  @methodDecorator('方法装饰器')
  @IncreasingEyes
  faceValue(){
    console.log('原本的脸')
  }

  getAge(@paramDecorator('参数装饰器') name: string){}
}

运行编译后的代码会发现,执行顺序为:属性装饰器 -> 访问器装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。

更详细的用法可参考官方文档:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories

装饰器运行时代码分析

由于浏览器不支持原生装饰器,必须通过工具编译为浏览器可执行的代码。

下面分析编译后的源码结构。

生成 face.js 文件:


tsc --target ES5 --experimentalDecorators face.ts

打开 face.js 文件,会看到一段被压缩的代码,可格式化以便阅读。

先看这段代码:


__decorate([
    propertyDecorator('属性装饰器')
], Girl.prototype, "age", void 0);
__decorate([
    AccessDecorator('访问符装饰器')
], Girl.prototype, "city", null);
__decorate([
    methodDecorator('方法装饰器'),
    IncreasingEyes
], Girl.prototype, "faceValue", null);
__decorate([
    __param(0, paramDecorator('参数装饰器'))
], Girl.prototype, "getAge", null);
Girl = __decorate([
    thinFace,
    classDecorator('类装饰器')
], Girl);

__decorate 函数的作用是执行装饰器函数。从这段代码中可看出许多信息,印证了前述结论。

通过 __decorate 的调用顺序,可看出多种装饰器一起使用时,顺序为:属性装饰器 -> 访问器装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。

调用 __decorate 函数时,根据使用的装饰器类型不同,传入的参数也有所不同。

第一个参数始终为数组,确保与书写顺序一致,每一项是求值后的装饰器函数。若写的是 @propertyDecorator(),则先执行该工厂函数得到装饰器函数,这与前述分析一致。

类装饰器将类作为第二个参数传入;其他装饰器将原型对象作为第二个参数,属性名作为第三个参数,第四个参数为 null 或 void 0。void 0 的值为 undefined,即未传参数。

需记住传给 __decorate 函数的参数个数和值,以便深入 __decorate 源码时,根据这些值决定执行装饰器函数时传入参数的多少。

接下来看 __decorate 函数的实现:


// 已存在此函数,直接使用,否则自己定义
var __decorate = (this && this.__decorate) ||
// 接收四个参数: 
//decorators存放装饰器函数的数组、target原型对象|类,
//key属性名、desc描述(undefined或null)
function(decorators, target, key, desc) {
  var c = arguments.length,
  // 拿到参数的个数
  r = c < 3 // 参数小于三个,说明是类装饰器,直接拿到类
    ? target
    : desc === null // 第四个参数为 null,则需要描述对象;属性装饰器传入是  void 0,没有描述对象。
        ? desc = Object.getOwnPropertyDescriptor(target, key) 
        : desc,
  d;
  // 如果提供了Reflect.decorate方法,直接调用;否则自己实现
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") 
    r = Reflect.decorate(decorators, target, key, desc);
  else 
    // 装饰器函数执行顺序和书写的顺序相反,从下至上 执行
    for (var i = decorators.length - 1; i >= 0; i--) 
      if (d = decorators[i]) // 拿到装饰器函数
          r = (c < 3 // 参数小于3个,说明是类装饰器,执行装饰器函数,直接传入类
            ? d(r) 
            : c > 3 // 参数大于三个,是方法装饰器、访问符装饰器、参数装饰器,则执行传入描述对象
              ? d(target, key, r) 
              : d(target, key) // 为属性装饰器,不传入描述对象
            ) || r;

  // 给被装饰的属性,设置得到的描述对象,主要是针对,方法、属性来说的
  /*** 
     * r 的值分两种情况,
     *  一种是通过上面的 Object.getOwnPropertyDescriptor 得到的值
     *  另一种,是装饰器函数执行后的返回值,作为描述对象。
     *      一般不给装饰器函数返回值。
    */
  return c > 3 && r && Object.defineProperty(target, key, r),r;
};

上述参数装饰器调用了一个名为 __params 的函数。


var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};

其目的是为装饰器函数传入参数的位置 paramIndex。

分析编译后的源码,相信能对装饰器的理解更加深刻。

以上就是JS装饰者模式和TypeScript装饰器的详细内容,更多关于JS的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章