yaofly2012 / note

Personal blog
https://github.com/yaofly2012/note/issues
44 stars 5 forks source link

ES7: Decorators #63

Open yaofly2012 opened 5 years ago

yaofly2012 commented 5 years ago

一、概念

标准定义:

A decorator is: an expression

  • that evaluates to a function
  • that takes the target, name, and decorator descriptor as arguments and optionally returns a decorator descriptor to install on the target object

二、语法

格式:@表达式 承接上面的概念逐句解释:

1. @后面跟任意表达式(A decorator is: an expression)

// 修饰器函数
function readonly(target, name, descriptor) {
    descriptor.writable = false;
    return descriptor;
}
var rd = readonly;

// 下面都是合法的修饰器函数使用方式
class Person {
    @readonly // 使用使用
    @rd // 变量
    @(function() { return rd; })() // 函数调用表达式
    name() { return `${this.first} ${this.last}` }
}

2. 表达式的值必须是个函数(that evaluates to a function)。

class Person {
    @1 // 非函数的值
    name() { return `${this.first} ${this.last}` }
}

抛异常:TypeError: decorator is not a function

3. 修饰器函数的实参(a function that takes the target, name, and decorator descriptor as arguments)

func(target, name, descriptor)

根据修饰器使用的地方不同,修饰器函数的实参也不同:

1. 修饰类的修饰器函数实参只有一个参数,即类本身;

此时修饰器也称为“Leading decorators”, 头部修饰器

2. 修饰类方法(包含getter/setter方法)修饰器函数有三个实参:

3. 修饰对象的方法(包含getter/setter方法)修饰器函数有三个实参:

4. 修饰类静态方法

5. 在修饰器函数里可以通过实参的个数判断当前decorator作用于类还是方法上。

function log(target, name, descriptor) {
    console.log(`apply on ${arguments.length === 3 ? "method" : "class"}`)
    return descriptor;
}

注意:

4. 可选的修饰器函数返回值(and optionally returns a decorator descriptor to install on the target object)

修饰器函数不仅可以操作类和方法,甚至可以完全创建一个新的类和方法。

  1. 应用与类的修饰器函数返回值如果是“真值”(非null, 0, false, undefined)时,则作为新值;
  2. 应用与方法的修饰器函数的返回值如果是特性对象时,则会作为方法的新的特性对象(这也要求此时返回值必须是个对象,否者会抛异常:TypeError: Property description must be an object)。
    
    function wrap(target) {
    return {
        name: 'new Obj'
    };
    }

@wrap // 完全重定义了 class Person { first = 'John' last = 'Snow' } console.log(Person) ; // { name: 'new Obj' }

// Demo2 function wrapMethod(target, name, decorator) { return { value: true }; } class Person { first = 'John' last = 'Snow' @wrapMethod get fullName() { return ${this.first} ${this.last}; } } var p = new Person(); console.log(p.fullName); // true

不过在这样做之前先看下使用[装饰模式](https://segmentfault.com/p/1210000009968000/read)的目的。

3. 如果修饰器函数没有返回值,则使用原来的特性对象/target (当修饰类时),所以修饰器函数的返回值是可选的。
4. 详细见[javascript-decorators#desugaring](https://github.com/wycats/javascript-decorators#desugaring)

## Issues
1. 修饰器函数什么时候执行?
在定义属性之前执行,修饰器用来修改方法的行为,必须在JS执行阶段动态的添加方法,并且在定义方法前执行修饰器函数。
```javascript
function readonly(target, name, descriptor) {
    console.log('call readonly'); 
    descriptor.writable = false;
    return descriptor;
}

class Person {
    @readonly
    name() { return `${this.first} ${this.last}` }
}
console.log('after Person definition')

输出结果: image

  1. 被修饰器应用的属性,相当于在JS执行阶段才动态的添加属性;
  2. 应用多个修饰器时,修饰器由低到上依次执行。
    var obj = {
    name: 'hoh',
    @log // 后于log1执行
    @log1 // 先执行
    say: function() {
    }
    }

三、剥去语法糖外衣

1. 类声明修饰器

@G
class Foo {
}

ES6表示:

var Foo = (function () {
  class Foo {
  }

  Foo = G(Foo) || Foo; // 调用修饰器函数,传入一个实参
  return Foo;
})();

相当于修改了类Foo的定义,在返回Foo变量前先调用了修饰器函数,并优先采用修饰器函数的返回值(真值)作为变量的最终值。

2. 类方法声明修饰器

class Foo {
  @G
  bar() { }
}

ES6表示:

var Foo = (function () {
  class Foo {
    bar() { }
  }

  var _temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar"); // 获取最初的属性特性
  _temp = G(Foo.prototype, "bar", _temp); // 调用修饰器函数,传入3个实参
  if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
  return Foo;
})();

其他如多个修饰器,getter/setter方法,对象方法类似。

Issues: 为啥修饰器不能修饰函数

函数存在提升问题,通过上面的desugaring过程我们知道包含修饰器的类或者对象都要被重写转成表达式形式。如果把函数转成表达式就破坏了函数本身提升带来的影响。 假如函数可以声明修饰器,则下面场景存在问题:

Func(); // 先调用了

@G
Func() {
}

转成ES6

Func(); // 先调用地方就报错了

@G
var Func = (function() {
  Func() {
  }
  Func = G(Func) || Func;
  return Func;
})()

小结

装饰器使得被修饰的类或者成员属性被重新定义了。并且在重定义时会调用装饰器函数,提供了操作被修饰类或者属性的机会。

四、修饰器能干什么?

decorator 让我们有机会在代码的执行期间改变其行为。 其实首先要看看修饰器函数的实参是什么,然后才能知道修饰器函数能干什么。

  1. 修饰方法时:
    • 通过修改类的prototype属性(target参数),扩展类或者对象的功能。
    • 通过属性的特性修改类方法行为(descriptor参数);
    • 其他任意行为。
  2. 修饰类时
    • 通过target参数扩展类本身,也可以操作类的prototype属性。

修饰器本质上是语法糖,使用修饰器就可以使用更简洁的代码实现上面罗列的功能。

Issues:

  1. Stage 2 decorators disallow object literal property decorators why ? google下需要配置babel。

  2. decorator为什么只能作用于类,类的方法,对象字面量方法而不能用于函数? 函数存在提升问题。

参考

  1. Decorators in ES7
  2. ES7 Decorator 装饰器 | 淘宝前端团队
  3. wycats/javascript-decorators 内部原理(必看)
  4. TC39 Stage2
yaofly2012 commented 5 years ago

能干啥?

http://raganwald.com/2015/06/26/decorators-in-es7.html https://github.com/jayphelps/core-decorators

yaofly2012 commented 5 years ago

修饰成员变量的修饰器

在标准中没看到相关说明,但是mobx里却有,实际通过Babel操作了下果然能正确执行。 那语法呢?

  1. 修饰对象的成员变量跟修饰对象的方法一样
  2. 修饰类的成员变量跟修饰类方法类似,但是有点区别:成员变量是定义在实例化的对象上的,修饰器返回的特性对象不是直接应用在类的prototype对象上,而是在构造函数里应用在this对象上。