ES7 decorator 深入探析

本文探讨了ES7中的Decorator特性,从起因、前置条件(ES5原理)到ES6的使用方式,详细解释了如何直接装饰class、装饰属性以及装饰器的高级用法,包括链式调用、combine和mixin。通过示例代码展示了装饰器如何改变对象属性的行为,并揭示了装饰器的本质是编译时运行的函数。
摘要由CSDN通过智能技术生成

起因

一直享受着 Anuglar 和 Nest 的红利,上来就是 @Component(...) 或者 @Controller(...),自己却没有实际的探究过背后的原理。于是今天想好好总结一下,沉淀沉淀。

前置条件(es5 原理)

之前看过红宝书,第六章提到过,js 对象的属性有几个特性:

  1. [[configurable]] 是否可配置
  2. [[enumerable]] 是否可枚举
  3. [[writeble]] 是否可修改值
  4. [[value]] 写入的值是啥

四个配置项都为 boolean 类型。
这四个配置联合起来有一个名字,叫做对象属性的描述符(descriptor)
其中,writeble 和 value 还有另外一个名字, settergetter 访问器)。
上代码:

const obj = { };
Object.defineProperty(obj,'a', {
value: 1,
writeble: false,
});
console.log(obj); // {a: 1}
console.log(obj.a) // 1
obj.a = 3; // 修改 a 属性的值
console.log(obj.a) // 1

/**====================另一种写法====================*/
const d = {};
Object.defineProperty(d , 'name' {
get: function() {return 1},
set: function(value) {return false}
});

console.log(d) // {} 注意!!!!这里跟 writeble 和 value 不太一样,这里打印出来的对象,是没有显示 name 属性的!!!但是访问可以访问出来
d.name; // 1
d.name = 3; // 尝试修改 name 属性 
d.name; // 1

我们发现,配置了可写入项为 false 时,我们就无法去修改对象属性的值了,有点像冻结的意思。刚好,JS 有个 Object.freeze(), 来看一下

const c = {name: 1};
Object.freeze(c);
c.name = 3;
console.log(c) // {a: 1}

发现和我们自己去配置 writeble: false 效果相同。不信?来验证一下:

Object.getOwnPropertyDescriptor(c);
// 返回: 
{
	 name: {
	 configurable: false
	 enumerable: true
	 value: 1
	 writable: false
	}
}

ES6 还要这么写吗?

不用。直接用装饰器 decorator来写。

第一种,直接装饰 class,

作用: 给类增加动态属性,该动态属性并不会被继承,只能作为 被装饰类 的 静态属性。
注意: 给类添加静态属性的这种行为,是在 编译时 发生的!所以说:
装饰器的本质就是编译时运行的函数

function addFlag(object) {
object.flag = true;
}

@addFlag
class Foo(){}
Foo.flag // true


// 来个实例
const f1 = new Foo();
f1.flag // undefined
第二种,装饰属性

装饰器会在 Object.defineProperty 之前执行,也就是拦截默认的访问修饰符。
举个例子:

// CSDN markdown 编辑器 为什么不支持 typescript 高亮?无语...
function nameEqual3(object, key, descriptor: PropertyDescriptor) {
    descriptor.value = 3;
    descriptor.writable = false;
}
class Person {

    @nameEqual3
    name() { }
}

const p = new Person();
console.log(p.name); // 3

可见其效果。
也支持传参,如下代码所示,请仔细阅读注释:

  // 装饰器函数 (用闭包来封装一下)
  function sign(id) {
    return function (target, name, descriptor) {
      /**
       *  这里的 value 在我看来,更像是一个 getter, 所以可以直接被赋值成一个函数
       *  类似于:
       *  descriptor = {
       *     get: function(){ return this.value } 
       *  }
       */
      const oldValue = descriptor.value;
      /**
       * 这里的 args 实际上就是装饰器在运行时,挂载的函数的入参,下面的 log 日志会证明
       */
      descriptor.value = function (...args) {
        console.log(`args =>`, args);
        console.log(`标记 ${id}`);
        return oldValue.apply(this, args);
      };

      return descriptor;
    }
  }

  class Person {
    @sign(1)
    method(a, b) {
      return a + b;
    }
  }

  // 实例化和调用
  const p1 = new Person();
  p1.method(2, 4);
  
  // 输出:
   args => [3,4]
   标记 1
第三种,装饰器的高级用法(链式调用, combine 以及 mixin)
1.链式(连续)

首先来看链式(连续)调用,这次多加一个装饰器,并且继续通过打印的方式来查看下调用的顺序:


// 装饰器函数 再 封装一层
function mark(id) {
  // 真正的装饰器函数以闭包形式返回
  return (obj, target, descriptor) => {
    // 不破坏原 getter 函数
    const old = descriptor.value;
    console.log(id);
    return descriptor.value = () => old.apply(this, id);
  }
}



class Person {

  @mark(1)
  @mark(2)
  method() { }
}


const p1 = new Person();

p1.method();

// 输出:
2 
1

咦?明明 @mark(1)@mark(2) 之前调用的啊,为什么 2 比 1 先执行了呢?
让我们打开 如下地址,跟着我一起分析:
Type Script - Play ground
来看右边编译后的 javascript 代码,只看 var decorator 被编译成了啥,下面的不用看,跟源码差不多。请仔细阅读注释

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
// 判断函数真正的入参,如果小于 3 个,r = target 否则 继续判断 ,在该 对象 的属性(被装饰的属性)上原本的 descriptor 是不是 null ? 如果是,则 desc 等于 当前对象被装饰属性的 descriptor ,否则 r = 当前对象被装饰属性的 descriptor
// 这里的 d 用于缓存 下面遍历时 的 状态
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    // 这里的 Reflect 是 window 下的 全局对象,我们也知道, Reflect 对象根本没有 decorate 方法,所以, turthy 的分支并不会执行,而是走 falsy 分支.
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    /**********关键步骤************/
    // 这里遍历的是入参的装饰器数组,并且,从右倒叙遍历(起始下标为 decorators.length - 1)
    // d 是每次遍历的 装饰器返回的 descriptor, 通过 判断入参个数,来决定 r 的类型,以及是否通过 d(r) 来装饰某个对象。如果 入参 < 3 个,即 r 为 一个对象,执行 d(r) ; 否则如果 入参 > 3 个,即运行时传入了第四个参数 desc(descriptor) , 此时的 r 其实就是 desc ,d(target, key, r) 意思是:用 入参的 desc 装饰对象 target 的 key 属性;否则 c < 4 , 此时的 r  为 object 对象,d(target, key);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

/*************************下面这些先不用看***********************/

// 装饰器函数 再 封装一层
function mark(id) {
    var _this = this;
    // 真正的装饰器函数以闭包形式返回
    return function (obj, target, descriptor) {
        // 不破坏原 getter 函数
        var old = descriptor.value;
        console.log(id);
        return descriptor.value = function () { return old.apply(_this, id); };
    };
}
var Person = /** @class */ (function () {
    function Person() {
    }
    Person.prototype.method = function () { };
    __decorate([
        mark(1),
        mark(2)
    ], Person.prototype, "method", null);
    return Person;
}());
var p1 = new Person();
p1.method();

上面啰里啰唆的注释是啥意思呢?
翻译成人话: 装饰器的执行顺序是个 栈, 后进先出。像极了… 爱情?不,像极了 洋葱模型

2. combine (合并)

合并指的是装饰器装饰某个类的属性的时候,同时应用多个装饰器的模式。(要跟下面的 @mixin)区分


function eatApple(count) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`吃了 ${count} 个 苹果`);
    return old.apply(this);
  }
}


function runMeter(long) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`跑了 ${long} 米`);
    return old.apply(this);
  }
}


function combine(...descriptors) {
  // 想点办法,让入参的每个函数立马执行!要把自己得到的对象分配给两个小弟
  return (obj, target, descriptor) => descriptors.forEach(d => d.apply(this, [obj, target, descriptor]));
}


class Person {

  @combine(eatApple(1), runMeter(9))
  method() { }
}


const p1 = new Person();

p1.method();

// 输出:
吃了一个苹果
跑了 9

可见,在 @combine() 中传入的参数顺序,竟然跟最终的顺序 是一样的,咦?不是洋葱吗?这压根不是栈啊!
脑子里回想一下刚才解析源码的过程,我再次望向了这次的源码:

var Person = /** @class */ (function () {
    function Person() {
    }
    Person.prototype.method = function () { };
    __decorate([
        combine(eatApple(1), runMeter(9))
    ], Person.prototype, "method", null);
    return Person;

显而易见,这两个函数,直接是作为结果被传进去的,相当于栈里面只有 mixin 一个函数,无所谓是栈或者队列了,反正两个函数都在我内部执行,我让他怎么执行就怎么执行,为所欲为。所以这里的输出结果是同步的,完全就是因为栈里只有一个 member。
不信验证一下:


function eatApple(count) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`吃了 ${count} 个 苹果`);
    return old.apply(this);
  }
}


function runMeter(long) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`跑了 ${long} 米`);
    return old.apply(this);
  }
}


function combine(...descriptors) {
  // 想点办法,让入参的每个函数立马执行!要把自己得到的对象分配给两个小弟
  return (obj, target, descriptor) => descriptors.forEach(d => d.apply(this, [obj, target, descriptor]));
}


class Person {

  @combine(eatApple(1), runMeter(9))
  @combine(eatApple(5),runMeter(100))
  method() { }
}


const p1 = new Person();

p1.method();

// 输出:
吃了 5 个 苹果
跑了 100 米
吃了 1 个 苹果
跑了 9
3. mixin (混合)

mixin 意为在一个对象之中混入另外一个对象的方法。

function mixins(...list) {
  return function (target) {
  // Object.assign 可用于对象,即 编译后的 es3 runtime 指向 class.prototype
    Object.assign(target.prototype, ...list);
  };
}
const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值