文档结构  
翻译进度:已翻译     翻译赏金:5 元 (?)    ¥ 我要打赏

随着 ES2015+ 的到来,转译已经司空见惯, 人们会在代码或者教程中看到各种新的语言特性。这些特性中有一个经常让人家挠头的特性,就是 JavaScript 的修饰符。

修饰符随 Angular 2+ 变得流行起来。在 Angular 中,是 TypeScript 带来了修饰符,不过修饰符会在今年晚些时间更新为 ES2017 的一部分。我们来看看修饰符是什么,以及如何使用它们可以让代码变得更容易理解。

第 1 段(可获 1.34 积分)

注意: 如果你比较喜欢视频,为什么不加入 SitePoint Premium 查看一些我们流行的 JavaScript 课程呢?

什么是修饰符?

用最简单的形式来说,修饰符是用一段代码包装另一段代码的方式 —— 字面上的“修饰”。

这个概念你以前可能听说过,就是“功能组合”,或者“高阶函数”。

对于许多用例来说,这种情况已经在标准 JavaScript 中实现了,只需要调用一个包装着另一个函数的函数即可:

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);
第 2 段(可获 1.09 积分)

这个示例产生了一些新的函数 —— 由变量 wrapped 引用 —— 可以像调用 doSomething 一样来调用 wrapped,而且它也会做相同的事情。不同之处在于 wrapped  会在被包装的函数调用之前和之后记录一些日志。

doSomething('Graham');
// Hello, Graham
wrapped('Graham');
// Starting
// Hello, Graham
// Finished

怎么使用 JavaScript 修饰符?

修饰符使用一个在 ES2017 中定义的特殊语法,在被修饰的代码前放置一个 开头的符号。

注意: 在写这篇文章的时候,修饰符已经处于“第 2 阶段草案”,这表示它们肯定会被最终完成,但目前仍然有可能发生变化。

第 3 段(可获 1.21 积分)

你可以根据需要对同一段代码使用多个修饰符,它们会按你声明的顺序来应用。

比如:

@log()
@immutable()
class Example {
  @time('demo')
  doSomething() {

  }
}

这段代码定义了一个类并使用了三个修饰符:两个用在类上,还有一个用在类的属性上:

  • @log 会记录对类进行的所有访问。
  • @immutable 让类成为不可变类 —— 它可能是在新实例上调用了 Object.freeze
  • @time 记录一个方法要执行多少时间,将将之用一个唯一的标记记录下来。
第 4 段(可获 1.13 积分)

目前,使用修饰符需要转译器的支持,因为浏览器和 Node 都还不支持这一特性。如果你使用 Babel,transform-decorators-legacy 会让这一切变得容易。

注意: 插件中使用“legacy(遗产)” 这个词是因为它以 Babel5 的方式处理修饰符,这一行为跟最终形成的标准有可能不同。

为什么使用修饰符?

JavaScript 中已经可以实现功能组合,但明显比较困难 —— 甚至不可能 —— 把相同的技术应用到其它代码上(比如类和类属性)。

第 5 段(可获 1.28 积分)

ES2017 草案添加了支持类和属性的修饰符,它可以用来解决这些问题,将来的 JavaScript 版本可能会允许在其它棘手的代码区域添加修饰符。

Decorators also allow for a cleaner syntax for applying these wrappers around your code, resulting in something that detracts less from the actual intention of what you are writing.

修饰符的不同类型

目前,唯一支持的修饰类型是用在类和类成员上的,包括属性、方法、getters 和 setters。

第 6 段(可获 1.13 积分)

修饰符只不过是返回另一个函数的函数,这被称为被修饰项适当的细节。这些修饰符函数会在程序首次运行时被执行一次,而其返回值会替代被修饰的代码。

类成员修饰符

属性修饰符用于类的某个成员 —— 不管它们是属性、方法、getter 还是 setter。

这类修改符函数调用时会传入三个参数:

  • target(目标) – 成员所在的类。
  • name(名称) – 类成员的名称。
  • descriptor(描述符) – 成员描述符。本质上是传入 Object.defineProperty 的对象。
第 7 段(可获 1.36 积分)

典型的应用示例是 @readonly。现在起来很简单:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

只需要通过设置属性描述符的 “writable” 标记为 false。

然后在类属性上像这样使用:

class Example {
  a() {}
  @readonly
  b() {}
}

const e = new Example();
e.a = 1;
e.b = 2;
// TypeError: Cannot assign to read only property 'b' of object '#<Example>'

然而我们再做得好一些。我们可以用不同的行为来改变修饰功能。比如,记录所有输入和输出:

第 8 段(可获 0.78 积分)
function log(target, name, descriptor) {
  const original = descriptor.value;
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log(`Arguments: ${args}`);
      try {
        const result = original.apply(this, args);
        console.log(`Result: ${result}`);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}

这段代码使用新的方法取代了原来的方法,新方法会记录参数,调用原来的方法,然后记录原有方法的输出。

第 9 段(可获 0.28 积分)

注意我们使用了展开运算符来自动从传入的所有参数构建数组,这种新语法用来代替以前的 arguments  值。

用法如下:

class Example {
    @log
    sum(a, b) {
        return a + b;
    }
}

const e = new Example();
e.sum(1, 2);
// Arguments: 1,2
// Result: 3

你会发现我们不得不使用一个有点好笑的语法执行被修饰的方法。这种方法在整篇文章都有使用。不过总的来说,apply  函数能让你调用函数,指定 this 以及调用函数时传入的参数。

第 10 段(可获 1.06 积分)

我们来进行一点改进,让修饰符处理一些参数。比如你下面这样重写 log 修饰符:

function log(name) {
  return function decorator(t, n, descriptor) {
    const original = descriptor.value;
    if (typeof original === 'function') {
      descriptor.value = function(...args) {
        console.log(`Arguments for ${name}: ${args}`);
        try {
          const result = original.apply(this, args);
          console.log(`Result from ${name}: ${result}`);
          return result;
        } catch (e) {
          console.log(`Error from ${name}: ${e}`);
          throw e;
        }
      }
    }
    return descriptor;
  };
}
第 11 段(可获 0.3 积分)

现在它变得更复杂了,但是拆解开来就容易解理了:

  • 一个函数,log,需要一个参数name
  • 这个函数返回另一个本身是修饰符的函数。

这与之前的 log 修饰符相同,除了它要使用来自外部函数的 name 参数。

像下面这样使用:

class Example {
  @log('some tag')
  sum(a, b) {
    return a + b;
  }
}

const e = new Example();
e.sum(1, 2);
// Arguments for some tag: 1,2
// Result from some tag: 3

马上我们可以看到,我们提供的标签让我们可以在日志中很容易识别需要的内容。

 

第 12 段(可获 1.03 积分)

这是因为 log('some tag') 调用时,JavaScript 运行时立即对其进行运算,然后从 sum 方法的修饰符得到响应。

类修饰符

类修饰符应用于整个类类型。这类修饰符调用时会传入一个参数,即被修饰的构造函数。

注意,它只应用于构造函数,而不会应用于创建出来的每个实例。也就是说,如果你想操作实例,就必须返回构造函数的包装版本。

第 13 段(可获 1.29 积分)

一般来说,它不如类成员的修饰符有用,因为你在这里做的所有事情都要吧通过一个简单的方法调用来实现。你用它们做的任何事情,最终都需要返回一个新的构造函数来代替原来的构造函数。

回到我们记录日志的示例,我们来写一个修饰符记录构造函数的参数:

function log(Class) {
  return (...args) => {
    console.log(args);
    return new Class(...args);
  };
}

这里我们接受一个类作为参数,然后返回一个新的函数来充当构造函数。它只是单纯地记录参数和构造函数通过这些参数构造出来的新实例。

第 14 段(可获 1.25 积分)

比如:

@log
class Example {
  constructor(name, age) {
  }
}

const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}

我们可以看到构造 Example 类实例时会记录提供给它的参数,而且构造出来的值确实是 Example 的实例。正是我们想要的。

向类修饰符传递参数与向类成员修饰符传递参数一样:

function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: args`);
      return new Class(...args);
    };
  }
}

@log('Demo')
class Example {
  constructor(name, age) {}
}

const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}
第 15 段(可获 0.55 积分)

实际案例

Core 修饰符

有一个神奇的库,称为 Core Decorators,它提供一些平常有用的修饰符。

这些修饰符使用简洁的语法,提供了非常有用的通用功能(比如,调用方法的时候,否决警告,允许某个值只读等)。

React

React 库很好的利用了高阶组件(Higher-Order Components)的概念。React 组件可以简单的写成函数,而它可以包含另一个组件。

欢迎购买我们的高级教程:以 ES6 的方式使用 React

第 16 段(可获 1.19 积分)

使用修饰符是理想的选择,因为改变成这样不需要大动作。比如,Redux 库有一个函数,connect,用于连接 React 组件和 Redux 存储。

一般会这样使用:

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

然而,有了修饰符语法之后,就可以这样用了:

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
第 17 段(可获 0.8 积分)

这与之前的功能完全相同。

MobX

MobX 库广泛使用了修饰符,让你很容易把字段标记为 Observable(可观察对象) 或 Computed(计算属性),以及把类变成 Observers(观察者)。

总结

类成员修饰符提供了一种极好的方式来包装类中的代码,这种方式与平时写独立函数的方式类似。它是一种编写简单辅助代码方式,可以简单地应用于各个地方,清晰,而且易于理解。

这种功能应用广泛,其用途仅限于你的想象!

第 18 段(可获 1.26 积分)

文章评论