JavaScript 装饰器模式详解:从原理到实战应用

作者:木棉时间:2026-10-22 13:49:03浏览:183

本文深入解析 JavaScript 装饰器模式,对比 Python 实现,详解基于 Object.defineProperty 的 ES7 语法机制。涵盖类与方法装饰器的参数差异、核心属性描述符配置,以及 core-decorators 库中的 autobind、readonly 等常用装饰器。结合 React Loading 等实际应用场景,提供代码示例与最佳实践。

JS装饰器背景与定义

ES6 引入了 class 和 extends 等类定义与操作特性,但在多类间共享或扩展方法行为时,现有机制显得不够优雅。为解决这一问题,我们需要一种更简洁的方式来处理这些场景。

什么是装饰器

Python 的装饰器

在面向对象(OOP)设计模式中,decorator 被称为装饰模式。该模式通常通过继承和组合实现,而 Python 不仅支持 OOP 风格的 decorator,还在语法层面直接提供了 decorator 支持。

如果你熟悉 Python,对它一定不会陌生。我们先来看一个 Python 中装饰器的示例:


def decorator(f):
  print "my decorator"
  return f
@decorator
def myfunc():
  print "my function"
myfunc()
# my decorator
# my function

这里的 即为我们所说的装饰器。在上述代码中,利用装饰器在目标方法执行前打印了一行文本,且未对原方法进行任何修改。该代码逻辑基本等同于:


def decorator(f):
  def wrapper():
    print "my decorator"
    return f()
  return wrapper
def myfunc():
  print "my function"
myfunc = decorator(myfuc)

通过代码可以看出,装饰器 decorator 接收一个参数,即被装饰的目标方法。处理扩展内容后,它返回一个新方法供后续调用,同时失去对原方法对象的直接访问。应用装饰器后,被装饰方法的入口引用实际上发生了改变,重新指向了装饰器返回的方法入口点,从而实现了对原函数的扩展或修改。

ES7 装饰器与 Object.defineProperty

ES7 的装饰器

ES7 中的 decorator 借鉴了类似的语法糖,但其底层依赖于 ES5 的 Object.defineProperty 方法。

Object.defineProperty

Object.defineProperty() 方法用于直接在对象上定义新属性或修改现有属性,并返回该对象。

该方法允许精确添加或修改对象属性。通过赋值添加的普通属性会在 for...in 循环或 Object.keys() 枚举期间显示,这些值可被修改或删除。而使用 Object.defineProperty() 添加的属性,其额外细节(如不可变性)可从默认值改变。默认情况下,通过此方法添加的属性值是不可变的。

语法


Object.defineProperty(obj, prop, descriptor)
  1. obj:要在其上定义属性的对象。
  2. prop:要定义或修改的属性的名称。
  3. descriptor:将被定义或修改的属性描述符。
  4. 返回值:被传递给函数的对象。

在 ES6 中,由于 Symbol 类型的特殊性,使用 Symbol 类型的值作为对象 key 与常规定义或修改不同,而 Object.defineProperty 是定义 key 为 Symbol 的属性的方法之一。

属性描述符

对象中目前存在的属性描述符主要有两种形式:数据描述符和存取描述符。

数据描述符是一个具有值的属性,该值可能是可写的,也可能不是。

  • 存取描述符是由 getter 和 setter 函数对描述的属性。
  • 描述符必须是这两种形式之一,不能同时具备两者。

数据描述符和存取描述符均具有以下可选键值:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能被改变,同时该属性也能从对应对象上被删除。默认为 false。

enumerable

enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。

当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 数据描述符同时具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值、对象、函数等)。默认为 undefined。

writable

当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。默认为 false。

存取描述符同时具有以下可选键值:

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

如果一个描述符不具有 value、writable、get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有 (value 或 writable) 和 (get 或 set) 关键字,将会产生一个异常。 用法

类与方法装饰器详解

类的装饰


@testable
class MyTestableClass {
 // ...
}

function testable(target) {
 target.isTestable = true;
}

MyTestableClass.isTestable // true

上述代码中, 就是一个装饰器。它修改了 MyTestableClass 类的行为,为其加上了静态属性 isTestable。testable 函数的参数 target 是 MyTestableClass 类本身。

基本上,装饰器的行为如下所示。


@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类。

如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。


function testable(isTestable) {
 return function(target) {
  target.isTestable = isTestable;
 }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上述代码中,装饰器 testable 可以接受参数,这就等于可以修改装饰器的行为。

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的 prototype 对象操作。

下面是另外一个例子。


// mixins.js
export function mixins(...list) {
 return function (target) {
  Object.assign(target.prototype, ...list)
 }
}

// main.js
import { mixins } from './mixins'

const Foo = {
 foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'

上述代码通过装饰器 mixins,把 Foo 对象的方法添加到了 MyClass 的实例上面。

方法的装饰

装饰器不仅可以装饰类,还可以装饰类的属性。


class Person {
 @readonly
 name() { return `${this.first} ${this.last}` }
}

上述代码中,装饰器 readonly 用来装饰“类”的 name 方法。

装饰器函数 readonly 一共可以接受三个参数。


function readonly(target, name, descriptor){
 // descriptor对象原来的值如下
 // {
 //  value: specifiedFunction,
 //  enumerable: false,
 //  configurable: true,
 //  writable: true
 // };
 descriptor.writable = false;
 return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
  • 装饰器第一个参数是类的原型对象,上例是 Person.prototype。装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时 target 参数指的是类本身);
  • 第二个参数是所要装饰的属性名
  • 第三个参数是该属性的描述对象

另外,上述代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。

函数方法的装饰

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行。


function doSomething(name) {
 console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
 return function() {
  console.log('Starting');
  const result = wrapped.apply(this, arguments);
  console.log('Finished');
  return result;
 }
}

const wrapped = loggingDecorator(doSomething);

core-decorators 库与使用场景

core-decorators.js

core-decorators.js 是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。

@autobind

autobind 装饰器使得方法中的 this 对象,绑定原始对象。

@readonly

readonly 装饰器使得属性或方法不可写。

@override

override 装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。


import { override } from 'core-decorators';

class Parent {
 speak(first, second) {}
}

class Child extends Parent {
 @override
 speak() {}
 // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
 @override
 speaks() {}
 // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
 //
 //  Did you mean "speak"?
}

@deprecate (别名@deprecated)

deprecate 或 deprecated 装饰器在控制台显示一条警告,表示该方法将废除。


import { deprecate } from 'core-decorators';

class Person {
 @deprecate
 facepalm() {}

 @deprecate('We stopped facepalming')
 facepalmHard() {}

 @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
 facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//   See http://knowyourmeme.com/memes/facepalm for more details.
//

@suppressWarnings

suppressWarnings 装饰器抑制 deprecated 装饰器导致的 console.warn() 调用。但是,异步代码发出的调用除外。

使用场景

装饰器有注释的作用


@testable
class Person {
 @readonly
 @nonenumerable
 name() { return `${this.first} ${this.last}` }
}

有了装饰器,就可以改写上面的代码。装饰


@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

相对来说,后一种写法看上去更容易理解。

新功能提醒或权限

菜单点击时,进行事件拦截,若该菜单有新功能更新,则弹窗显示。


/**
 * @description 在点击时,如果有新功能提醒,则弹窗显示
 * @param code 新功能的code
 * @returns {function(*, *, *)}
 */
 const checkRecommandFunc = (code) => (target, property, descriptor) => {
  let desF = descriptor.value; 
  descriptor.value = function (...args) {
   let recommandFuncModalData = SYSTEM.recommandFuncCodeMap[code];

   if (recommandFuncModalData && recommandFuncModalData.id) {
    setTimeout(() => {
     this.props.dispatch({type: 'global/setRecommandFuncModalData', recommandFuncModalData});
    }, 1000);
   }
   desF.apply(this, args);
  };
  return descriptor;
 };

loading

在 React 项目中,我们可能需要在向后台请求数据时,页面出现 loading 动画。这个时候,你就可以使用装饰器,优雅地实现功能。


@autobind
@loadingWrap(true)
async handleSelect(params) {
 await this.props.dispatch({
  type: 'product_list/setQuerypParams',
  querypParams: params
 });
}

loadingWrap 函数如下:、


export function loadingWrap(needHide) {

 const defaultLoading = (
  <div className="toast-loading">
   <Loading className="loading-icon"/>
   <div>加载中...</div>
  </div>
 );

 return function (target, property, descriptor) {
  const raw = descriptor.value;
  
  descriptor.value = function (...args) {
   Toast.info(text || defaultLoading, 0, null, true);
   const res = raw.apply(this, args);
   
   if (needHide) {
    if (get('finally')(res)) {
     res.finally(() => {
      Toast.hide();
     });
    } else {
     Toast.hide();
    }
   }
  };
  return descriptor;
 };
}

问题:这里大家可以想想看,如果我们不希望每次请求数据时都出现 loading,而是要求只要后台请求时间大于 300ms 时,才显示 loading,这里需要怎么改?

以上就是本次小编整理的关于 JS 装饰器的相关知识点,感谢大家对本站的支持。

作者最新文章