随着 ES2015+ 的到来,转译已经司空见惯, 人们会在代码或者教程中看到各种新的语言特性。这些特性中有一个经常让人家挠头的特性,就是 JavaScript 的修饰符。
修饰符随 Angular 2+ 变得流行起来。在 Angular 中,是 TypeScript 带来了修饰符,不过修饰符会在今年晚些时间更新为 ES2017 的一部分。我们来看看修饰符是什么,以及如何使用它们可以让代码变得更容易理解。
注意: 如果你比较喜欢视频,为什么不加入 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);
这个示例产生了一些新的函数 —— 由变量 wrapped
引用 —— 可以像调用 doSomething
一样来调用 wrapped,而且它也会做相同的事情。不同之处在于 wrapped
会在被包装的函数调用之前和之后记录一些日志。
doSomething('Graham');
// Hello, Graham
wrapped('Graham');
// Starting
// Hello, Graham
// Finished
怎么使用 JavaScript 修饰符?
修饰符使用一个在 ES2017 中定义的特殊语法,在被修饰的代码前放置一个 @
开头的符号。
注意: 在写这篇文章的时候,修饰符已经处于“第 2 阶段草案”,这表示它们肯定会被最终完成,但目前仍然有可能发生变化。
你可以根据需要对同一段代码使用多个修饰符,它们会按你声明的顺序来应用。
比如:
@log()
@immutable()
class Example {
@time('demo')
doSomething() {
}
}
这段代码定义了一个类并使用了三个修饰符:两个用在类上,还有一个用在类的属性上:
- @log 会记录对类进行的所有访问。
- @immutable 让类成为不可变类 —— 它可能是在新实例上调用了
Object.freeze
。 - @time 记录一个方法要执行多少时间,将将之用一个唯一的标记记录下来。
目前,使用修饰符需要转译器的支持,因为浏览器和 Node 都还不支持这一特性。如果你使用 Babel,transform-decorators-legacy 会让这一切变得容易。
注意: 插件中使用“legacy(遗产)” 这个词是因为它以 Babel5 的方式处理修饰符,这一行为跟最终形成的标准有可能不同。
为什么使用修饰符?
JavaScript 中已经可以实现功能组合,但明显比较困难 —— 甚至不可能 —— 把相同的技术应用到其它代码上(比如类和类属性)。
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。
修饰符只不过是返回另一个函数的函数,这被称为被修饰项适当的细节。这些修饰符函数会在程序首次运行时被执行一次,而其返回值会替代被修饰的代码。
类成员修饰符
属性修饰符用于类的某个成员 —— 不管它们是属性、方法、getter 还是 setter。
这类修改符函数调用时会传入三个参数:
- target(目标) – 成员所在的类。
- name(名称) – 类成员的名称。
- descriptor(描述符) – 成员描述符。本质上是传入 Object.defineProperty 的对象。
典型的应用示例是 @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>'
然而我们再做得好一些。我们可以用不同的行为来改变修饰功能。比如,记录所有输入和输出:
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;
}
这段代码使用新的方法取代了原来的方法,新方法会记录参数,调用原来的方法,然后记录原有方法的输出。
注意我们使用了展开运算符来自动从传入的所有参数构建数组,这种新语法用来代替以前的 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
以及调用函数时传入的参数。
我们来进行一点改进,让修饰符处理一些参数。比如你下面这样重写 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;
};
}
现在它变得更复杂了,但是拆解开来就容易解理了:
- 一个函数,
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
马上我们可以看到,我们提供的标签让我们可以在日志中很容易识别需要的内容。
这是因为 log('some tag') 调用时,JavaScript 运行时立即对其进行运算,然后从 sum 方法的修饰符得到响应。
类修饰符
类修饰符应用于整个类类型。这类修饰符调用时会传入一个参数,即被修饰的构造函数。
注意,它只应用于构造函数,而不会应用于创建出来的每个实例。也就是说,如果你想操作实例,就必须返回构造函数的包装版本。
一般来说,它不如类成员的修饰符有用,因为你在这里做的所有事情都要吧通过一个简单的方法调用来实现。你用它们做的任何事情,最终都需要返回一个新的构造函数来代替原来的构造函数。
回到我们记录日志的示例,我们来写一个修饰符记录构造函数的参数:
function log(Class) {
return (...args) => {
console.log(args);
return new Class(...args);
};
}
这里我们接受一个类作为参数,然后返回一个新的函数来充当构造函数。它只是单纯地记录参数和构造函数通过这些参数构造出来的新实例。
比如:
@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 {}
实际案例
Core 修饰符
有一个神奇的库,称为 Core Decorators,它提供一些平常有用的修饰符。
这些修饰符使用简洁的语法,提供了非常有用的通用功能(比如,调用方法的时候,否决警告,允许某个值只读等)。
React
React 库很好的利用了高阶组件(Higher-Order Components)的概念。React 组件可以简单的写成函数,而它可以包含另一个组件。
欢迎购买我们的高级教程:以 ES6 的方式使用 React
使用修饰符是理想的选择,因为改变成这样不需要大动作。比如,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 {}
这与之前的功能完全相同。
MobX
MobX 库广泛使用了修饰符,让你很容易把字段标记为 Observable(可观察对象) 或 Computed(计算属性),以及把类变成 Observers(观察者)。
总结
类成员修饰符提供了一种极好的方式来包装类中的代码,这种方式与平时写独立函数的方式类似。它是一种编写简单辅助代码方式,可以简单地应用于各个地方,清晰,而且易于理解。
这种功能应用广泛,其用途仅限于你的想象!

- 原文:JavaScript Decorators: What They Are and When to Use Them / JavaScript 修饰符是什么及何时使用它们
- 作者:Graham Cox
- 频道:计算机
- 发布:CY2 (2017-06-07)
- 标签: JavaScript
- 版权:本文仅用于学习、研究和交流目的,非商业转载请注明出处、译者和可译网完整链接。
文章评论