JavaScript 装饰器原理探究
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 根据开发者来定
- 无返回值: 往实例挂载属性,并赋初始化值
思考一个例子:
可见 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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论