Appearance
装饰器
装饰器(Decorator)用来增强 JavaScript 类(class)的功能
装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面
js
@frozen
class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}凡是标注“新语法”的章节,都是基于当前的语法,简写为 ”新“。未标注“新语法”的章节基于以前的语法,是过去遗留的稿子。
装饰器的种类(新)
装饰器可以装饰四种类型的值
- 类
- 类的属性(public, private, and static)
- 类的方法(public, private, and static)
- 属性存取器(accessor)(public, private, and static)
装饰器 API(新)
装饰器是一个函数
js
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;装饰器函数调用,接受两个参数
value:被装饰的值,某些情况下可能是undefined(装饰属性时)context:一个对象,包含了被装饰的值的上下文信息
context对象的属性如下
kind:字符串,表示装饰类型,取值class、method、getter、setter、field、accessorname:被装饰的值的名称: The name of the value, or in the case of private elements the description of it (e.g. the readable name).access:对象,包含访问这个值的方法,即存值器和取值器static: 布尔值,该值是否为静态元素private:布尔值,该值是否为私有元素addInitializer:函数,允许用户增加初始化逻辑
装饰器的执行步骤如下。
- 计算各个装饰器的值,按照从左到右,从上到下的顺序
- 调用方法装饰器
- 调用类装饰器
类的装饰
装饰器可以用来装饰整个类
js
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true
}
MyTestableClass.isTestable // true代码分析:@testable 是一个装饰器,修改了 MyTestableClass 的行为,添加静态属性 isTestable
装饰器的行为
js
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类
一个参数不够,装饰器外封装一层函数
js
function testable(isTestable) {
return function (target) {
target.isTestable = isTestable
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false注意:装饰器对类的行为的改变,是代码编译时发生,而不是在运行时
前面例子是为类添加静态属性,添加实例属性,通过目标类操作
js
function testable(target) {
target.prototype.isTestable = true
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass()
obj.isTestable // true另外一个例子
js
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins.js'
const Foo = {
foo() {
console.log('foo')
}
}
@mixins(Foo)
class MyClass {}
let obj = new MyClass()
obj.foo() // 'foo'React 与 Redux 库结合使用
js
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent)装饰器改写
js
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}类装饰器(新语法)
类装饰器的类型描述
js
type ClassDecorator = (
value: Function,
context: {
kind: 'class',
name: string | undefined,
addInitializer(initializer: () => void): void
}
) => Function | void参数说明
value:被修饰的类
context:上下文对象,修饰匿名类,name 属性为 undefined
js
function logged(value, { kind, name }) {
if (kind === 'class') {
return class extends value {
constructor(...args) {
super(...args)
console.log(`constructing an instance of ${name} with arguments ${args.join(', ')}`)
}
}
}
// ...
}
@logged
class C {}
new C(1)
// constructing an instance of C with arguments 1方法装饰器(新语法)
方法装饰器
js
type ClassMethodDecorator = (
value: Function,
context: {
kind: 'method',
name: string | symbol,
access: { get(): unknown },
static: boolean,
private: boolean,
addInitializer(initializer: () => void): void
}
) => Function | void参数说明
value:所要装饰的方法
context:与类装饰器相同
返回值
- 返回一个方法,取代原来方法
- 不返回值,使用原来方法
- 返回其他值,报错
js
function logged(value, { kind, name }) {
if (kind === 'method') {
return function (...args) {
console.log(`starting ${name} with arguments ${args.join(', ')}`)
const ret = value.call(this, ...args)
console.log(`ending ${name}`)
return ret
}
}
}
class C {
@logged
m(arg) {}
}
new C().m(1)
// starting m with arguments 1
// ending m装饰器本质为语法糖,实际操作如下
js
class C {
m(arg) {}
}
C.prototype.m =
logged(C.prototype.m, {
kind: 'method',
name: 'm',
static: false,
private: false
}) ?? C.prototype.m方法的装饰
装饰器不仅可以装饰类,还可以装饰类的属性
js
class Person {
@readonly
name() {
return `${this.first} ${this.last}`
}
}装饰器 readonly 用来装饰 “类” 的 name 方法
装饰器函数可以接受三个参数
target:类的原型对象。本意是装饰实例,但实例还未生成,只能装饰原型。
name:所修饰的属性名
descriptor:属性描述对象
js
function readonly(target, name, descriptor) {
// descriptor对象原来的值如下
// {
// 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 装饰器,可以起到输出日志的作用
js
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(this, arguments)
}
return descriptor
}
const math = new Math()
// passed parameters should get logged now
math.add(2, 4)使用 Decorator 写法的组件
js
@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>
)
}
}同一个方法有多个装饰器,先从外到内进入,然后由内向外执行
js
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为什么装饰器不能用于函数?
装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升
js
var readOnly = require("some-decorator");
@readOnly
function foo() {
}实际执行
js
var readOnly;
@readOnly
function foo() {
}
readOnly = require("some-decorator");装饰函数:使用高阶函数形式执行
js
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)存取器装饰器(新语法)
存取器装饰器使用 TypeScript 描述的类型如下
js
type ClassGetterDecorator = (
value: Function,
context: {
kind: 'getter',
name: string | symbol,
access: { get(): unknown },
static: boolean,
private: boolean,
addInitializer(initializer: () => void): void
}
) => Function | void
type ClassSetterDecorator = (
value: Function,
context: {
kind: 'setter',
name: string | symbol,
access: { set(value: unknown): void },
static: boolean,
private: boolean,
addInitializer(initializer: () => void): void
}
) => Function | void参数说明
value:原始的存值器(setter)和取值器(getter)
返回值
与方法装饰器等同
存取器装饰器对存值器(setter)和取值器(getter)是分开作用的
js
class C {
@foo
get x() {
// ...
}
set x(val) {
// ...
}
}修改 @Logged 装饰器
js
function logged(value, { kind, name }) {
if (kind === 'method' || kind === 'getter' || kind === 'setter') {
return function (...args) {
console.log(`starting ${name} with arguments ${args.join(', ')}`)
const ret = value.call(this, ...args)
console.log(`ending ${name}`)
return ret
}
}
}
class C {
@logged
set x(arg) {}
}
new C().x = 1
// starting x with arguments 1
// ending x属性装饰器(新语法)
属性装饰器的类型描述
js
type ClassFieldDecorator = (
value: undefined,
context: {
kind: 'field',
name: string | symbol,
access: { get(): unknown, set(value: unknown): void },
static: boolean,
private: boolean
}
) => (initialValue: unknown) => unknown | void第一个参数是 undefined,可以让装饰器返回一个初始化函数,当该属性被赋值时,这个初始化函数会自动运行,它会收到属性的初始值,然后返回一个新的初始值 属性装饰器也可以不返回任何值。只要返回的不是函数,而是其他类型的值,就会报错
js
function logged(value, { kind, name }) {
if (kind === 'field') {
return function (initialValue) {
console.log(`initializing ${name} with value ${initialValue}`)
return initialValue
}
}
// ...
}
class C {
@logged x = 1
}
new C()
// initializing x with value 1实际执行
js
let initializeX = logged(undefined, {
kind: "field",
name: "x",
static: false,
private: false,
}) ?? (initialValue) => initialValue;
class C {
x = initializeX.call(this, 1);
}accessor 命令(新语法)
类装饰器引入了一个新命令 accessor,用来属性的前缀。简写形式,相当于声明属性 x 是私有属性#x 的存取接口
js
class C {
accessor x = 1
}等效写法
js
class C {
#x = 1
get x() {
return this.#x
}
set x(val) {
this.#x = val
}
}accessor 命令前面,还可以加上 static 命令和 private 命令
js
class C {
static accessor x = 1
accessor #y = 2
}accessor 命令前面还可以接受属性装饰器
js
function logged(value, { kind, name }) {
if (kind === 'accessor') {
let { get, set } = value
return {
get() {
console.log(`getting ${name}`)
return get.call(this)
},
set(val) {
console.log(`setting ${name} to ${val}`)
return set.call(this, val)
},
init(initialValue) {
console.log(`initializing ${name} with value ${initialValue}`)
return initialValue
}
}
}
// ...
}
class C {
@logged accessor x = 1
}
let c = new C()
// initializing x with value 1
c.x
// getting x
c.x = 123
// setting x to 123addInitializer() 方法(新语法)
addInitializer()方法,用来完成初始化操作。
运行时间
类装饰器:在类被完全定义之后。
方法装饰器:在类构造期间运行,在属性初始化之前。
静态方法装饰器:在类定义期间运行,早于静态属性定义,但晚于类方法的定义。
例子:自定义 html 元素
js
function customElement(name) {
return (value, { addInitializer }) => {
addInitializer(function () {
customElements.define(name, this)
})
}
}
@customElement('my-element')
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['some', 'attrs']
}
}customElements.define[1]:创建自定义元素
等效写法
js
class MyElement {
static get observedAttributes() {
return ['some', 'attrs']
}
}
let initializersForMyElement = []
MyElement =
customElement('my-element')(MyElement, {
kind: 'class',
name: 'MyElement',
addInitializer(fn) {
initializersForMyElement.push(fn)
}
}) ?? MyElement
for (let initializer of initializersForMyElement) {
initializer.call(MyElement)
}方法修饰器例子,实现类方法 this 绑定
js
function bound(value, { name, addInitializer }) {
addInitializer(function () {
this[name] = this[name].bind(this)
})
}
class C {
message = 'hello!'
@bound
m() {
console.log(this.message)
}
}
let { m } = new C()
m() // hello!等效写法
js
class C {
constructor() {
for (let initializer of initializersForM) {
initializer.call(this)
}
this.message = 'hello!'
}
m() {}
}
let initializersForM = []
C.prototype.m =
bound(C.prototype.m, {
kind: 'method',
name: 'm',
static: false,
private: false,
addInitializer(fn) {
initializersForM.push(fn)
}
}) ?? C.prototype.mcore-decorators.js
core-decorators.js[2] 提供了常见装饰器
@autobind
autobind:方法中的 this 对象,绑定原始对象
js
import { autobind } from 'core-decorators'
class Person {
@autobind
getPerson() {
return this
}
}
let person = new Person()
let getPerson = person.getPerson
getPerson() === person
// true@readonly
readonly 装饰器使得属性或方法不可写
@override
override 装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错
js
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)
}
// or
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 装饰器在控制台显示一条警告,表示该方法将废除
js
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 装饰器抑制 deprecated 装饰器导致的 console.warn()调用
js
import { suppressWarnings } from 'core-decorators'
class Person {
@deprecated
facepalm() {}
@suppressWarnings
facepalmWithoutWarning() {
this.facepalm()
}
}
let person = new Person()
person.facepalmWithoutWarning()
// no warning is logged实现自动发布事件
使用装饰器,使得对象的方法被调用时,自动发出一个事件
例子:publish的装饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。
时间发布和订阅的库 [3] postal
js
const postal = require('postal/lib/postal.lodash')
export default function publish(topic, channel) {
const channelName = channel || '/'
const msgChannel = postal.channel(channelName)
msgChannel.subscribe(topic, (v) => {
console.log('频道: ', channelName)
console.log('事件: ', topic)
console.log('数据: ', v)
})
return function (target, name, descriptor) {
const fn = descriptor.value
descriptor.value = function () {
let value = fn.apply(this, arguments)
msgChannel.publish(topic, value)
}
}
}js
// index.js
import publish from './publish'
class FooComponent {
@publish('foo.some.message', 'component')
someMethod() {
return { my: 'data' }
}
@publish('foo.some.other')
anotherMethod() {
// ...
}
}
let foo = new FooComponent()
foo.someMethod()
foo.anotherMethod()调用someMethod或者anotherMethod,就会自动发出一个事件
bash
$ bash-node index.js
频道: component
事件: foo.some.message
数据: { my: 'data' }
频道: /
事件: foo.some.other
数据: undefinedMixin
Mixin 模式,对象继承的一种替代方案
在装饰器的基础上,可以实现 Mixin 模式
例子:将方法混入类中
js
const Foo = {
foo() {
console.log('foo')
}
}
class MyClass {}
Object.assign(MyClass.prototype, Foo)
let obj = new MyClass()
obj.foo() // 'foo'通用脚本 mixins.js,将 Mixin 写成一个装饰器
js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}js
import { mixins } from './mixins.js'
const Foo = {
foo() {
console.log('foo')
}
}
@mixins(Foo)
class MyClass {}
let obj = new MyClass()
obj.foo() // "foo"上面方法会改写 prototype 对象,可以通过类继承实现
js
class MyClass extends MyBaseClass {
/* ... */
}js
let MyMixin = (superclass) =>
class extends superclass {
foo() {
console.log('foo from MyMixin')
}
}目标类再去继承这个混入类
js
class MyClass extends MyMixin(MyBaseClass) {
/* ... */
}
let c = new MyClass()
c.foo() // "foo from MyMixin"“混入”多个方法,就生成多个混入类
js
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
/* ... */
}好处:可以调用 super,因此可以避免在“混入”过程中覆盖父类的同名方法。
js
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()
}
}Trait
Trait 也是一种装饰器,效果与 Mixin 类似。但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等
采用 traits-decorator[4]这个第三方模块作为例子
js
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() // barTrait 不允许“混入”同名方法
js
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 {}
// 报错
// throw new Error('Method named: ' + methodName + ' is defined twice.');
// ^
// Error: Method named: foo is defined twice.TFoo 和 TBar 都有 foo 方法,结果 traits 装饰器报错
解决方法 1:使用绑定运算符(::)在 TBar 上排除 foo 方法
js
import { traits, excludes } from 'traits-decorator'
class TFoo {
foo() {
console.log('foo')
}
}
const TBar = {
bar() {
console.log('bar')
},
foo() {
console.log('foo')
}
}
@traits(TFoo, TBar::excludes('foo'))
class MyClass {}
let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar解决方法 2:为 TBar 的 foo 方法起一个别名
js
import { traits, alias } from 'traits-decorator'
class TFoo {
foo() {
console.log('foo')
}
}
const TBar = {
bar() {
console.log('bar')
},
foo() {
console.log('foo')
}
}
@traits(TFoo, TBar::alias({ foo: 'aliasFoo' }))
class MyClass {}
let obj = new MyClass()
obj.foo() // foo
obj.aliasFoo() // foo
obj.bar() // baralias 和 excludes 方法,可以结合起来使用
js
@traits(TExample::excludes('foo', 'bar')::alias({ baz: 'exampleBaz' }))
class MyClass {}上述代码:排除了TExample的foo方法和bar方法,为baz方法起了别名exampleBaz
as 方法则为上面的代码提供了另一种写法
js
@traits(TExample::as({ excludes: ['foo', 'bar'], alias: { baz: 'exampleBaz' } }))
class MyClass {}