装饰器到底给前端带来了什么

1,417 阅读7分钟

前言

装饰器语法是处于ECMA提案TC39的 Stage2阶段的语法,因为Stage2表示是draft阶段,可能未来相关会有变化。本篇文章就按照当下的情况去讲解,这个语法可以用Object.DefineProperty代替,所以也是一个语法糖。

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,比如Java和Python,Python是可以装饰函数的,但是在JavaScript里装饰器只能装饰和类相关的部分。

装饰器

装饰器是什么

装饰器(Decorator)是一种与类(class)相关的新语法,用来注释或修改类和类方法。装饰器语法是处于ECMA提案TC39的 Stage2阶段的语法,因为Stage2表示是draft阶段,可能未来相关会有变化。本篇文章就按照当下的情况去讲解。

许多面向对象的语言都有这项功能,比如Java和Python,Python是可以装饰函数的,但是在JavaScript里装饰器只能装饰和类相关的部分。

基本用法就是@+函数名字就表示一个装饰器。

装饰器的本质

当然上面讲的是装饰器的基本信息,没有说明装饰器的本质。装饰器本质就是一个加了@符号的函数 。而这个函数的作用就是在类或者类的属性和方法外再加一层限制逻辑 ,而这个逻辑在代码的表现上是将类与函数解耦 。看了下面的例子你就懂了:

// 使用装饰器
@testable
class MyTestableClass {
  // ...
}
function testable(target) {
  target.isTestable = true;
}
// 执行下面的代码会输出true
MyTestableClass.isTestable
​
​
// 不使用装饰器
class MyTestableClass {
  static isTestable = true
  // ...
}
// 执行下面的代码也会输出true
MyTestableClass.isTestable

上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestabletestable函数的参数targetMyTestableClass类本身。这个时候可以说@testable装饰了MyTestableClass类,这也是为什么取名为装饰器。

在不使用装饰器的情况下通过直接在MyTestableClass类写入也可以实现,为什么要用装饰器这么麻烦的语法写呢?想一想,假设isTestable属性在项目里是一个公共属性要很多类都需要,采用第二种方法写入在每个类里,如果未来需要将isTestable改为false,就需要去每个类里改一次,但是通过装饰器写的话只需要修改一下装饰器的实现代码就行了,这就达到了解耦的目的。

装饰器怎么用

装饰类

除了上面的用法,还可以把装饰器函数写成高阶函数的形式,让装饰器更灵活。

比如:

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false

装饰属性和方法

用装饰器写一个组件:

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.scss'
})
export class MyComponent {
  @Prop() first: string;
  @Prop() last: string;
  @State() isVisible: boolean = true;
  render() {
    return (
      <p>Hello, my name is {this.first} {this.last}</p>
    );
  }
}

可以看出装饰器还有注释的作用,对于上面的组件prop和state就很清晰了。

如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。

装饰器的好处

使用装饰器易于实现AOP编程(面向切片编程)和装饰器模式。

AOP编程:在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。感兴趣可以看这个回答www.zhihu.com/question/24…

装饰器模式:看这个文章segmentfault.com/a/119000003…

依赖注入(Dependency Injection)

依赖注入DI、控制反转IOC和IOC容器

一般来说既然都使用了装饰器去做一些AOP编程,后续为了彻底解耦还会使用依赖注入的模式。

先看代码:

class B { }
class A {
  constructor(b: B) { 
    //类A依赖类B的实例
  }
}
​
// 将B的实例注入到a中
const a = new A(new B());
​

当一个类A依赖类B,但是只是去使用类B并不会对类B造成影响,也不控制类B的创建和销毁,这就是类B依赖注入 类A,这里类B的控制权不在类A里而在别的地方控制的方式也叫控制反转 (IOC)。

在上面的例子中,将类B依赖注入类A的代码是我们自己手动写的,也就是const a = new A(new B()),在实际项目里可能会有很多这样的类需要注入,全部我们自行手写比较混乱,所以需要一个IOC容器 统一管理这其中的依赖关系和行为。

这就是解释了DI/IOC/IOC容器的概念和之间的关系

依赖注入的好处

参考了一些答案,举一个简单的例子来说明,假设需要写组装一辆车的代码。

不使用依赖注入

// 轮胎的类
class Tire {
}
// 引擎的类
class Engine {
  createEngine(){
  }
}
// 车的类
class Car {
  public engine: Engine;
  public tires: Tires;
  constructor() {
    this.engine = new Engine();
    this.tires = new Tire();
  }
  run() {
    return this.engine.createEngine() + this.tires
  }
}
​
// 使用时
const car = new Car();
​

上述类Car如果需要run起来,需要Engine和Tire的实例方法,所以直接在Car类里去创建。这个时候Car类就和其他的类高度耦合起来了。

缺陷1

假设未来Engine的createEngine需要传入参数才可以实现createEngine的函数功能时,这时不仅需要修改Engine,还需要修改Car里使用的Engine实例。

// 引擎的类
class Engine {
  constructor(price){
    this.price = price
  }
  createEngine(){
    this.price...
  }
}
// 车的类
class Car {
  public engine: Engine;
  public tires: Tires;
  constructor() {
    // 需要修改Car类的代码
    this.engine = new Engine(price);
    this.tires = new Tire();
  }
  run() {
    return this.engine.createEngine() + this.tires
  }
}

缺陷2

假设未来有更多种类的轮胎,比如class Tire1、class Tire2...都是继承自Tire

// 轮胎的类
class Tire {
}
class Tire1 extends Tire {
}
class Tire2 extends Tire {
}
class Car {
  public engine: Engine;
  public tires: Tires;
  constructor() {
    this.engine = new Engine();
    //又需要修改Car类的代码
    this.tires = new Tire1();
  }
  run() {
    return this.engine.createEngine() + this.tires
  }
}
​

缺陷3

类Car的测试比较麻烦,因为类Car依赖了别的类,别的类可能又继续依赖别的类。类之间没有做到解耦,同一个测试用例可能因为依赖的类的变化,测试用例就不能用了。

使用依赖注入

// 轮胎的类
class Tire {
}
// 引擎的类
class Engine {
  createEngine(){
  }
}
// 车的类
class Car {
  // 依赖注入
  constructor(engine:Engine, tire:Tire) {}
  run() {
    return this.engine.createEngine() + this.tires
  }
}
// 使用时
//将这部分放在一个IOC容器管理
const car = new Car(new Engine(), new Tire());

这时候上面的问题就迎刃而解了,无论依赖的类怎么变化都不会破坏类Car。只是使用依赖注入,需要额外创建一个IOC容器去管理依赖。一些框架会把IOC容器封装好,不需要我们自己封装就会非常方便,比如服务端的Spring框架(Java)、服务端的Nest框架(typescript)、web端的Anglar。

总结

回到题目,装饰器到底给前端带来了什么?一个处于Stage2很久很久的提案,早就应用在了Anglar框架里,但是因为装饰器的晦涩以及使用装饰器产生的设计模式导致它对于新手上手非常不友好。

经常会在后端听到说写代码要抽象和模块化,这样可以增强代码的鲁棒性和降低代码熵增,更易于测试和扩展。但是对于前端说起模块化第一反应是ESModule,可惜的是ESModule并不是设计模式所需要的真正的模块化,而装饰器对前端实现优雅的模块化带来的希望,AngularNest就是最佳示范。

我相信在未来随着TS的广泛使用,装饰器模式也会广泛存在在项目中。