Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

    • 编程语言

      • CSS

      • HTML

      • JavaScript

        • ECMAScript6入门
        • JS 手写题

        • JS技巧
        • JS 学习笔记
        • Promise then 原理分析
        • async 函数编译原理
        • 你不知道的JavaScript(上)
        • 再谈闭包
        • 浏览器剪切板协议
        • 前端实现相对路径转绝对路径的几种方法
        • 为什么 0.._ 等于 undefined
        • 前端项目中常用的位操作技巧
        • 如何利用前端剪切板实现文件上传
        • 快速理解 JS 装饰器
          • 理解装饰器
          • 装饰类型
          • 装饰器语法
            • 装饰器类型定义
          • 装饰器执行顺序
          • 其他
            • reflect-metadata
            • 为什么不能装饰普通函数
          • 拓展阅读
        • 趣味js-只用特殊字符生成任意字符串
        • 重学 JS 原型链
        • 面试官问:怎么避免函数调用栈溢出
      • Rust

      • TypeScript

      • WebAssembly

    • 开发工具

    • 前端调试

    • 浏览器原理

    • 浏览器生态

  • 应用框架

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 前端基础
  • 编程语言
  • JavaScript
gahing
2024-08-26
目录

快速理解 JS 装饰器

装饰器用于增强 JavaScript 类的功能,包括类本身、类属性、类方法、类属性存取器、类方法参数以及类属性前缀(accessor,装饰私有属性)等

为什么不能装饰普通函数,可以看下文

参见示例,感受下写法

// 装饰类
@frozen class Foo {

  // 装饰属性
  @readonly x = 1;

  // 装饰类方法
  @throttle(500)
  @log(true)
  expensiveMethod(@withParam() name: string) {} // 装饰类方法参数

  // 装饰属性存取器
  @foo
  get x() {}
  @foo2
  set x(val) {}

  /**
   * 装饰类属性前缀,相当于声明属性 y 是私有属性 #y 的存取接口
   * 等价于
   * #y = 1;
   * get y() { return this.#y; }
   * set y(val) { this.#y = val; }
   */
  accessor y = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 理解装饰器

装饰器是一种设计模式,旨在扩展代码功能而不修改它。

在 JS 的装饰器语法出现之前,我们采取的是高阶函数/高阶组件的方案来实现代码功能的包装。

示例:在方法执行前后打印日志

function log({ namespace }){
    return (func) => {
        // 传入原函数并返回另一个包装过的函数
        return (...args) => {
            console.log(`${namespace}: 开始执行`)
            func.apply(this, args)
            console.log(`${namespace}: 执行结束`)
        }
    }
}

let myMethod = function (params) {
    console.log('执行 myMethod', params)
}
myMethod = log({ namespace: 'home' })(myMethod)
myMethod('hello')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

输出结果

home: 开始执行
执行 myMethod hello
home: 执行结束
1
2
3

用 js 装饰器语法来写,代码如下:

function log({ namepace }) {
    // ...
}
// PS: 此处仅做示例,装饰器规范是不支持普通函数的
@log({ namespace: 'home' })
function myMethod(params) {
    // ...
}
1
2
3
4
5
6
7
8

可以看到,我们在不更改 myMethod 内部函数逻辑的情况下,增加了日志打印功能。

这种编程范式有个名字叫:面向切面编程

# 装饰类型

装饰器的定义分为两种模式:

  • 普通装饰器(无法传参)
  • 装饰器工厂(可以传参)

普通装饰器(无法传参)

@decorator
class A {}
// 等同于
class A {}
A = decorator(A)
1
2
3
4
5

装饰器工厂(可传参)

@decorator(true)
class A {}
// 等同于
class A {}
A = decorator(true)(A)
1
2
3
4
5

无论哪种模式,最后的执行结果是一个装饰器函数,用于包装原始代码逻辑

# 装饰器语法

目前 JS Decorator 提案还在 stage3 阶段(202408),其语法定义与 TS 有些差别。

TypeScript 5.0+ 同时支持两种装饰器语法。标准语法可以直接使用,传统语法(legency)需要打开 --experimentalDecorators 编译参数。

关于 TS 中如何使用装饰器,可以查看官方文档 (opens new window)

目前生产环境中我们大多使用的是 TS + 传统语法,下面也只会讲TS 传统语法,待标准语法提案完全定案再调整这篇文章内容。

# 装饰器类型定义

装饰器类型定义遵循以下规则,很容易记忆:

  • 第一个参数是 target, 表示类构造函数(CustomClass.prototype.constructor, 对于类装饰器或者类静态方法),或者类的原型(CustomClass.prototype ,对于类实例方法);类装饰器只有此参数
  • 第二个参数是 propertyKey,表示被装饰的类成员名称,比如方法名或属性名
  • 第三个参数是 descriptor,表示被装饰方法的描述对象(参数装饰器另说,其参数 parameterIndex 表示其所在参数列表的索引)

类装饰器定义

/**
 * @Return 处理后的原始构造函数或者新的构造函数
 */
type ClassDecorator = <TFunction extends Function> (
    /** 类构造函数,唯一入参 */
    target: TFunction
) => TFunction | void;
1
2
3
4
5
6
7

方法装饰器定义

/**
 * @Return 修改后的该方法的描述对象,可以覆盖原始方法的描述对象。
 */
type MethodDecorator = <T>(
  /** 类构造函数(对于类的静态方法),或者类的原型(对于类的实例方法)*/
  target: Object,
  /** 所装饰方法的方法名 */
  propertyKey: string|symbol,
  /** 所装饰方法的描述对象 */
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
1
2
3
4
5
6
7
8
9
10
11

属性装饰器定义

type PropertyDecorator = (
    /** 类构造函数(对于类的静态方法),或者类的原型(对于类的实例方法)*/
    target: Object,
    /** 所装饰属性的属性名 */
    propertyKey: string|symbol
) => void;
1
2
3
4
5
6

存取器装饰器的类型定义,与方法装饰器一致

type AccessorDecorator = MethodDecorator
1

参数装饰器定义

type ParameterDecorator = (
  /** 类构造函数(对于类的静态方法),或者类的原型(对于类的实例方法)*/
  target: Object,
  /** 所装饰方法的方法名 */
  propertyKey: string|symbol,
  /** 当前参数在方法的参数序列的位置 */
  parameterIndex: number
) => void;
1
2
3
4
5
6
7
8

# 装饰器执行顺序

装饰器的执行分为两个阶段。

  1. 加载:计算 @ 符号后面的表达式的值,得到包装函数。
  2. 执行:将得到的函数应用于所装饰对象。

同时装饰器可分为三种类型:

  • 实例相关装饰器,包括实例方法、实例属性以及实例方法参数等的装饰器
  • 静态相关装饰器,包括静态方法、静态属性以及静态方法参数等的装饰器
  • 类相关装饰器,包括类装饰器和构造函数参数装饰器

PS:标准语法当前还不支持参数装饰器(202408)

注意:Decorator 标准语法和 legency 语法的执行顺序差异非常大。

先说标准语法执行顺序:

  1. 先加载全部,再执行全部
  2. 加载阶段:先加载类装饰器,再按照代码定义顺序逐个加载类成员装饰器
  3. 执行阶段:按照静态方法=>实例方法=>静态属性=>实例类型=>类装饰器的顺序,每种类型存在多个类成员时按代码定义顺序执行
  4. 如果同一个方法或属性有多个装饰器,则顺序加载(从外向里)、逆序执行(由里向外)

附上标准语法在线测试 (opens new window)

legency 语法的执行顺序比较复杂

  1. 整体过程按照:实例相关装饰器 => 静态相关装饰器 => 类相关装饰器的顺序加载和执行
  2. 同一类装饰器,按照代码定义顺序加载和执行;同类中的每个类成员,逐个加载逐个执行再处理下一个类成员
  3. 方法装饰器/类装饰器早于方法属性装饰器/构造函数属性装饰器加载,晚于执行。可以理解为顺序加载(从上到下)、逆序执行(从下到上)
  4. 如果同一个方法或属性有多个装饰器,则顺序加载(从外向里)、逆序执行(由里向外)
  5. 如果同一个方法有多个参数,那么参数装饰器也是顺序加载、逆序执行

legency 语法综合测试示例,在线体验 (opens new window)

function f(key:string):any {
  console.log(`加载:${key}`);
  return function (target: any) {
    console.log(`执行:${key}`);
  };
}
@f('类装饰器')
class C {
  @f('静态方法')
  static staticMethod() {}

  @f('静态属性')
  static staticProperty = 1

  @f('静态方法2')
  static staticMethod2() {}


  @f('实例方法1')
  method(
    @f('实例方法1参数A') a:any,
    @f('实例方法1参数B') b:any,
  ) {}

  @f('实例属性')
  property: number = 2;

  @f('f:实例方法2')
  @f('f2:实例方法2')
  method2(
    @f('实例方法2参数A') a:any,
  ) {}

  constructor(@f('构造函数参数') foo:any) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

执行顺序如下:

// --- 实例 ---
// 按照代码定义顺序
"加载:实例方法1" 
"加载:实例方法1参数A" 
"加载:实例方法1参数B" 
"执行:实例方法1参数B" 
"执行:实例方法1参数A" 
"执行:实例方法1" 

"加载:实例属性" 
"执行:实例属性" 

// 顺序加载(从外向里)、逆序执行(由里向外)
"加载:f:实例方法2" 
"加载:f2:实例方法2" 
"加载:实例方法2参数A" 
"执行:实例方法2参数A" 
"执行:f2:实例方法2" 
"执行:f:实例方法2" 

// --- 静态 ---
"加载:静态方法" 
"执行:静态方法" 

"加载:静态属性" 
"执行:静态属性" 

"加载:静态方法2" 
"执行:静态方法2" 

// --- 类 ---
"加载:类装饰器" 
"加载:构造函数参数" 
"执行:构造函数参数" 
"执行:类装饰器" 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 其他

# reflect-metadata

借助 reflect-metadata (opens new window) 库,在设计阶段添加的类型信息可以在运行时使用。

一些比较高级的玩法可以参见 Nest (opens new window)

# 为什么不能装饰普通函数

有个说法是,普通函数存在函数提升,导致装饰器函数执行时机晚于被装饰函数。当装饰器函数中存在副作用时(比如修改上层作用域的变量),会导致执行结果与预期不一致。

示例

var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}
1
2
3
4
5
6
7
8
9

预期在初始化 foo 函数后,自动执行 add 装饰器函数,counter 值被修改为 1

实际执行效果为


// 提升
@add
function foo() {
}
// 此时 add 还未初始化,装饰器函数如何执行?

var counter = 0;

var add = function () {
  counter++;
};
1
2
3
4
5
6
7
8
9
10
11
12

相关讨论:

  • https://github.com/ruanyf/es6tutorial/issues/399
  • https://github.com/wycats/javascript-decorators/issues/4

# 拓展阅读

  • https://es6.ruanyifeng.com/#docs/decorator
  • https://www.tslang.cn/docs/handbook/decorators.html
  • https://wangdoc.com/typescript/decorator
  • https://docs.nestjs.com/custom-decorators
编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
如何利用前端剪切板实现文件上传
趣味js-只用特殊字符生成任意字符串

← 如何利用前端剪切板实现文件上传 趣味js-只用特殊字符生成任意字符串→

最近更新
01
浅谈代码质量与量化指标
08-27
02
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
03
Hybrid 基建需要做哪些?
09-12
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式