JavaScript 修饰符是什么及何时使用它们
栏目: JavaScript · 发布时间: 7年前
内容简介:JavaScript 修饰符是什么及何时使用它们
随着 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(观察者)。
总结
类成员修饰符提供了一种极好的方式来包装类中的代码,这种方式与平时写独立函数的方式类似。它是一种编写简单辅助代码方式,可以简单地应用于各个地方,清晰,而且易于理解。
这种功能应用广泛,其用途仅限于你的想象!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?
- C++函数修饰符总结
- Go语言的修饰器编程
- GO语言的修饰器编程
- decrator(修饰器)的业务应用
- 《Python有什么好学的》之修饰器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。