JavaScript 装饰器原理探究

发布于 2022-10-02 15:29:51 字数 4924 浏览 178 评论 0

JavaScript 装饰器可以装饰类、类方法、类属性,以例子来说用法如下:

@cls
class A {
  @dec
  a = 1;

  @decFn fn() {console.log(this.a)}
}

function dec(target, prop, descriptor) {
  // target --- A.protocol
  // prop --- a
  // descriptor --- {
  //        configurable: true,
  //        enumerable: true,
  //        writable: true,
  //        initializer: function initializer() {
  //          return 1; // 初始化时,会绑定实例作为 this 执行该函数,把返回值赋值给属性
  //        },
  //      }
  console.log(target, prop, descriptor);
}

function decFn(target, prop, descriptor) {
  // target --- A.protocol
  // prop --- fn
  // descriptor --- {configurable: true, enumerable: false, writable: true, value: Function}
  console.log(target, prop, descriptor);
}

function cls(target) {
  // target --- A
  console.log(target);
  // 可对类进行操作
  // 若有返回值,则返回值作为新类
}

cls 就是类的装饰器,dec 就是实例属性装饰器,decFn 就是类方法装饰器。

js 装饰器原本设计是代码运行前执行的,可以做静态分析之类的事情。但是由于装饰器语法还处于提案中,并且语法可能在提案的不同阶段都会变(比如 babel legacy: true/false 编译出来的结果不同),不稳定,所以引擎还未去实现它。

所以现在想用装饰器语法,必须借助工具,比如 babel 转译、tsc 编译。转/编译后的装饰器,实际也是 runtime 阶段执行,只不过在修饰的类被实例化之前 invoke 。

接下来就分析 babel legacy: true 编译后的代码探究 js 装饰器的原理。

首先是对类的装饰

let A = cls(_class = class A {
}) || _class;

很好理解,就是类自身传进装饰器内,让开发者可以自行操作该类,若有返回值则把类的引用替换成返回值的,否则保持类的引用。

然后是对类属性的装饰

let A = cls(_class = (_class2 = class A {
  constructor() {
    _initializerDefineProperty(this, "a", _descriptor, this);
  }


}, (_descriptor = _applyDecoratedDescriptor(_class2.prototype, "a", [dec], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: function initializer() { 
  	// 实例属性的初始值
    return 1;
  }
})), _class2)) || _class;

_initializerDefineProperty 的行为就是在实例化时,给实例属性挂描述符。

function _initializerDefineProperty(target, property, descriptor, context) {
  // target --- 实例
  // property --- 属性
  // descriptor --- 描述符
  // context --- 实例
  if (!descriptor) return; // 装饰器有返回值,此处则不再挂描述符
  Object.defineProperty(target, property, {
    enumerable: descriptor.enumerable,
    configurable: descriptor.configurable,
    writable: descriptor.writable,
    value: descriptor.initializer
      ? descriptor.initializer.call(context)
      : void 0,
  });
}

_applyDecoratedDescriptor 的话主要目的就是执行装饰器。

function _applyDecoratedDescriptor(
  target, // 实例属性、类方法  --- 原型
  property, // 属性/方法名
  decorators, // [装饰器, ...]
  descriptor, // 描述符
  context, // 实例属性 --- undefined  类方法 --- 原型
) {
  var desc = {};
  Object.keys(descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  if ("value" in desc || desc.initializer) {
    desc.writable = true;
  }
  // 以上构建属性描述符
   
  // 以下执行装饰器,像洋葱一样内部先执行
  desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc; // 若装饰器有返回值,则把返回值作为描述符
    }, desc);
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    // 若属性装饰器走到此处,代表把装饰器的返回值作为了描述符,挂载在 原型 上
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

由上可知:

对类属性的装饰,也是在实例化前执行的。并且根据装饰器有没有返回值分情况对属性作操作:

  • 有返回值: 往类原型挂载属性,是赋值还是挂 getter setter 根据开发者来定
  • 无返回值: 往实例挂载属性,并赋初始化值

思考一个例子:

image.png

可见 getter setter 也是会按照原型链去找的。

最后是对类方法的修饰

let A =
  cls(
    (_class =
      ((_class2 = class A {
        constructor() {
          _initializerDefineProperty(this, "a", _descriptor, this);
        }

        fn() {
          console.log(this.a);
        }
      }),
      ((_descriptor = _applyDecoratedDescriptor(_class2.prototype, "a", [dec], {
        configurable: true,
        enumerable: true,
        writable: true,
        initializer: function initializer() {
          return 1;
        },
      })),
      _applyDecoratedDescriptor( // 对函数做处理
        _class2.prototype,
        "fn",
        [decFn],
        Object.getOwnPropertyDescriptor(_class2.prototype, "fn"), // {writable: true, enumerable: false, configurable: true, value: ƒ}
        _class2.prototype
      )),
      _class2))
  ) || _class;

经过对属性装饰器的分析,方法就很简单了,就是在实例化前执行装饰器,然后把类原型、方法名、方法描述符作为参数传进去,若有返回值,则把返回值当描述符挂在对象的方法名上,若无返回值,则挂方法原本的描述符。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
28 人气
更多

推荐作者

lylex099819

文章 0 评论 0

yg

文章 0 评论 0

mb_PT8LkUS5

文章 0 评论 0

埋情葬爱

文章 0 评论 0

佚名

文章 0 评论 0

奢望

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文