一、Babel 环境配置
- 安装依赖
$ npm i babel-core babel-plugin-transform-decorators-legacy babel-cli --save-dev
- 配置 .babelrc 文件
{
"plugins": ["transform-decorators-legacy"]
}
- 运行文件
$ npx babel-node .\01_test.js
Babel 的官方网站提供一个
在线转码器勾选对应的选项即可
二、类的修饰器
修饰器(Decorator)是一个函数,用来修改类的行为。ES2017引入了这项功能,目前Babel转码器已经支持
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true
}
MyTestableClass.isTestable // true
上面的代码中,@testable 会是一个修饰器。修改了 MyTestableClass 这个类的行为,为它加上了静态属性 isTestable
修饰器的行为基本如下
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A
修饰器对类的行为的改变是在代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
修饰器函数的第一个参数就是所要修饰的目标类
function testable(target) {}
testable 函数的参数 target 就是会被修饰的类。
如果觉得一个参数不过用,可以在修饰器外面再封装一层函数
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
修饰器 testable 可以接受参数,这就等于可以修改修饰器的行为。
前面的例子是为类添加一个静态属性没如果想添加实例属性,可以通过目标类的 prototype 对象进行操作
function testable(target) {
target.prototype.isTestable = true
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass()
obj.isTestable // true
修饰器函数 testable 是在目标类的 prototype 对象上添加属性的,因此就可以在实例上调用
下面是另外一个例子
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
}
@mixins(Foo)
class MyClass {}
let obj = new MyClass()
obj.foo() // 'foo'
上面通过修饰器 mixins 把 Foo 类的方法添加到了 MyClass 的实例上面。
可以使用 Object.assign() 模拟这个功能
const Foo = {
foo() { console.log('foo') }
}
class MyClass {}
Object.assign(MyClass.prototype, Foo)
let obj = new MyClass()
obj.foo() // 'foo'
三、方法的修饰
修饰器不仅可以修饰类,还可以 修饰类的属性。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
修饰器 readonly 用来修饰“类” 的name 方法。
修饰器函数一个可以接受3个 参数:
- 所要修饰的目标对象,
- 所要修饰的属性名,
- 该属性的描述对象。
function readonly(target, name, descriptor) {
/* desctiptor 对象原来的值如下
{
value: specifiedFunction,
enumerable: false,
configurable: true,
writable: true
}
*/
descriptor.writable = false
return descriptor
}
readonly(Person.prototype, 'name', descriptor)
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor)
下面的 @log 修饰器可以起到输出日志的作用。
class Math {
@log
add(a, b) {
return a + b
}
}
function log(target, name, descriptor) {
var oldValue = descriptor.value
descriptor.value = function() {
console.log(`Calling “${name}” with`, arguments)
return oldValue.apply(null, arguments)
}
return descriptor
}
const math = new Math()
math.add(2, 4)
@log 修饰器的作用就是在执行原始的操作之前执行一次 console.log,从而达到输出日志的目的
修饰器有注释的作用
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}`}
}
从上面的代码中,一眼就能看出,Person 类是可测试的,而 name 方法是只读且不可枚举的
如果同一个方法有多个修饰器,那么该方法会先从外到内进入修饰器,然后由内到外执行。
function dec(id) {
console.log('evaluated', id)
return (target, property, descriptor) => console.log('executed', id)
}
class Example {
@dec(1)
@dec(2)
method() {}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
外层修饰器 @dec(1) 先进入,但是内层修饰器 @dec(2) 先执行
除了注释,修饰器还能用来进行类型检查,所有,对于类来说,这项功能相当有用,他将是 JavaScript 代码静态分析的重要工具
四、为什么修饰器不能用于函数
修饰器只能用于类 和 类的方法,不能用于函数,因为存在函数提升
var counter = 0
var add = function() {
counter++
}
@add
function foo() {
}
上面代码的本意是使执行后的 counter 等于1,但实际上结果是 counter 等于0.因为 函数提升,使得实际执行的代码如下。
@add
function foo() {
}
var counter
var add
counter = 0
add = function() {
counter++
}
总之,由于存在函数提升,修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行
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)
五、core-decorators.js
core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器
@autobind
autobind 修饰器使得方法中的 this 对象绑定原始对象
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this
}
}
let person = new Person()
let getPerson = person.getPerson
getPerson() === person
// true
@readonly
readonly 修饰器使得属性或方法不可写
import { readonly } from 'core-decorators';
class Meal {
@readonly
entree = 'steak';
}
var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]
@override
override 修饰器检查之类的方法是否正确覆盖了父类的同名方法,如果不正确会报错
import { override } from 'core-decorators'
class Parent {
speak(first, second) {}
}
class Child extends Parent {
@override
speak() {}
// SyntaxError: Child#speak() {} does not properly override Parent#speak(first, second) {}
}
// 或者
class Child extends Parent {
@override
speaks() {}
// SyntaxError: No descriptor matching Child#speaks() {} was found on the prototype chain.
// Did you mean "speak"?
}
@deprecate(别名@deprecated)
deprecate 或 deprecated 修饰器在控制台显示一条警告,表示该方法将废除。
import { deprecate } from 'core-decorators'
class Person {
@deprecate
facepalm() {}
@deprecate('We stopped facepalming')
facepalmHard() {}
@deprecate('We stopped facepalming', {url: 'http://knowyourmeme.com/memes/facepalm'})
facepalmHarder() {}
}
let person = new Person()
person.facepalm()
// DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard()
// DEPRECATION Person#facepalmHard: We stopped facepalming
person.facepalmHarder()
/*
DEPRECATION Person#facepalmHarder: We stopped facepalming
See http://knowyourmeme.com/memes/facepalm for more details.
*/
@suppressWarnings
suppressWarnings 修饰器抑制 decorated 修饰器导致的 console.warn() 调用,但异步代码发出的调用除外
import { suppressWarnings, deprecated } from 'core-decorators'
class Person {
@deprecated
facepalm() {}
@suppressWarnings
facepalmWithoutWarning() {
this.facepalm()
}
}
let person = new Person()
person.facepalmWithoutWarning()
// no warning is logged
六、Mixin
在修饰器的基础上可以实现 Mixin 模式。所谓 Mixin 模式,就是对象继承的一种替代方案,在一个对象中混入另一个对象的方法。
const Foo = {
foo() { console.log('foo') }
}
class MyClass {}
Object.assign(MyClass.prototype, Foo)
let obj = new MyClass()
obj.foo() // 'foo'
通过 Objecy.assign 方法可以将 foo 方法 “混入” MyClass 类,导致 MyClass 的实例对象 obj 都具有 foo 方法。这就是 “混入” 模式的一个简单实现。
下面,部署一个通用脚本 mixins.js,将 Mixin 写成一个修饰器
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
使用上面这个修饰器为类“混入” 各种方法。
import { mixins } from './mixins'
const Foo = {
foo() {console.log('foo')}
}
@mixins(Foo)
class MyClass {}
let obj = new MyClass()
obj.foo() // 'foo'
上面的方法会改写 MyClass 类的 prototype 对象,如果不喜欢这一点,也可以通过下面的方法
通过 类的继承实现 Mixin
class MyClass extends MyBaseClass {
/* ... */
}
上面的代码中,MyClass 继承了 MyBaseClass。
如果我们想在 MyClass 里面“混入” 一个 foo 方法,其中一个办法就是在 MyClass 和 MyBaseClass 之间插入一个混入类,这个类具有 foo 方法,并且继承了 MyBaseClass 的所有方法,然后 MyClass 再继承这个类。
let MyMixin = (superclass) => class extends superclass {
foo() {
console.log('foo from MyMixin')
}
}
上面的代码中,MyMixin 是一个混入类生成器,接受 superclass 作为参数,然后返回 一个继承 superclass 的子类,该子类包含一个 foo 方法
接着,目标类再去继承这个混入类就达到了 “混入” foo 方法的目的
class MyClass extends MyMixin(MyBaseClass) {
/* ... */
}
let c = new MyClass()
c.foo() //"foo from MyMixin"
如果需要“混入” 多个方法,就生成多个混入类
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
/* ... */
}
这种写法的一个好处是 可以调用 super,避免在“混入” 过程中覆盖父类的同名方法。
let Mixin1 = superclass => class extends superclass {
foo() {
console.log('foo from Mixin1')
if (super.foo) super.foo()
}
}
let Mixin2 = superclass => class extends superclass {
foo {
console.log('foo from Mixin2')
if (super.foo) super.foo()
}
}
class S {
foo() {
console.log('foo from S')
}
}
class C extends Mixin1(Mixin2(S)) {
foo() {
console.log('foo from C')
super.foo()
}
}
上面的代码中,每一次混入发生时都调用父类的 super.foo 方法,导致父类的同名方法没有被覆盖,行为被保留了下来。
new C().foo()
// foo from c
// foo from Mixin1
// foo from Mixin2
// foo from S
七、Trait
Trait 也是一种修饰器,效果与 Mixin 类似,但是提供了更多功能,
比如防止 同名方法的冲突、排除混入某些方法、为混入的方法起别名等
下面以 traits-decorator 这个第三方模块为例进行说明。这个模块提供的 traits 修饰器不仅可以接受对象,还可以接受 ES6 类作为参数。
import { traits } from 'traits-decorator'
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') }
}
@traits(TFoo, TBar)
class MyClass {}
let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar
通过 traits 修饰器在 MyClass 类上 “混入” 了 TFoo 类的 foo 方法和 TBar 类的 bar 方法
Trait 不允许“混入” 同名方法。
import { traits } from 'traits-decorator'
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') },
foo() { console.log('foo') }
}
@traits(TFoo, TBar)
class MyClass {}
// Error: Method named: foo is defined twice.
一种解决方法是排除 TBar 的 foo 方法。
import { traits, excludes } from 'traits-decorator'
// ...
@traits(TFoo, TBar::excludes('foo'))
class MyClass { }
let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar
上面的代码使用绑定运算符 (::)在 TBar 上排除了 foo 方法,混入就不会报错了。
另一种方法是为 TBar 的 foo 方法起一个别名
import { traits, alias } from 'traits-decorator'
@traits(TFoo, TBar::alias({foo: 'aliasFoo'}))
class MyClass{ }
alias 和 excludes 方法可以结合起来使用
@traits(TExample::excludes('foo', 'bar')::alias({baz: 'exampleBaz'}))
class MyClass {}
排除了 TExample 的 foo 方法 和 bar 方法,为 baz 方法起了别名 exampleBaz
as 方法则为上面的飞马提供了另一种写法
@traits(TExample::as({
excludes: ['foo', 'bar'],
alias: {baz: 'exampleBaz'}
}))
class MyClass()
网友评论