装饰器的简单实现(下)
上文介绍了装饰模式,装饰器语法,今天继续说一下怎么用javascript实现自定义装饰器。
¶启用装饰器语法
目前decorator语法只是题案,想要使用装饰器,需要安装babel和webpack并结合babel/plugin-proposal-decorators 插件。
关于如何配置请参考babel插件的配置文档。
或者你也可以直接到 https://babeljs.io/repl/ 这个地址编写代码,它提供了一个在线的repl环境,可以直接运行es6代码,记得勾选左侧的Experimental来启用装饰器语法。
🔑 es2015,es2016,es2017和es6之间的关系:
ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。
¶类装饰器,方法装饰器和属性装饰器
¶类装饰器
类装饰器就是用来修改类的行为,它标注在类定义的上方。
@testable
class MyTestableClass {
// ...
}
function testable(target) { // target 就是被装饰的类
target.isTestable = true;
}
MyTestableClass.isTestable // true
这是一个简单的类装饰器,它给被装饰的类添加了一个isTestable的静态属性。
上面是个简单的例子,下面来个稍微复杂的。
想一下,如果我们想给装饰器传参数,要怎么做?熟悉高阶函数的同学一定会立刻想到,函数可以返回函数,只要在装饰器函数外面再加一层函数就可以了。
function testable(isTrue) {
return function(target) {
target.isTestable = isTrue
}
}
@testable(false)
class MyTestableClass {
}
MyTestableClass.isTestable // false
上面是给类添加静态属性,如果想给实例添加属性,需要把属性添加在prototype上
function testable(isTrue) {
return function(target) {
target.prototype.isTestable = isTrue
}
}
🔑 修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
接下来来个稍微复杂一点的例子,用装饰器实现mixin,
🔑 mixin模式就是一些提供能够被一个或者一组子类简单继承功能的类,意在重用其功能。
function mixin(foo) {
return function(target){
Object.assign(target.prototype,foo) // Onject.assign 是es6里面的Object的新函数,用来把第二个参数的属性合并到第一个参数上 因为这里传入的是类(也就是构造函数),想要在实例上添加属性,需要合并到原型对象上。
}
}
const Foo = {
sayHi(){
console.log('hi')
}
}
@mixin(Foo)
class Target{
}
new Target().sayHi()
// Hi
有了装饰器,之前那些烦人的样板函数就可以用装饰器代替了,比如我们用redux和react时,需要把ui组件和逻辑组件合并,经常需要写下面的代码:
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
有了装饰器,就可以改写上面的代码。
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
¶方法和属性的装饰
上面的装饰器定义函数我们只用到了一个参数,实际上,在装饰类的属性/方法时他有三个函数。
/**
* target 被装饰的类的原型对象,需要注意,装饰class和class的属性,第一个参数是不一样的,装饰class时,是构造函数,也就是类本身
* key 要修饰的属性名
* descriptor 该属性的描述对象
*/
function decorator(target,key,descriptor){}
🔑 关于描述对象,不熟悉的可以去看js高级程序设计关于 defineProperty 和 defineProperties的介绍,这两个方法就是用来定义和修改对象的内部属性。
举一个常见的例子,在每次函数执行前后,把函数的参数和结果打印出来
function log(target,key,descriptor) {
var old = descriptor.value
descriptor.value = function(...args) {
console.log(`call ${key} with,`,...args)
const result = old.call(this,...args)
console.log(`result is ${result}`)
return result
}
return descriptor
}
class Util {
@log
add(a,b) {
return a + b
}
}
const util = new Util()
util.add(1,2)
// 这时,控制台会打印出日志
// call add with, 1 2
// result is 3
来一个可缓存的装饰器
function cacheable(target,key,descriptor) {
const old = descriptor.value
if( typeof old !== 'function' ) {
throw new Error("must be a function")
}
const cache = {}
descriptor.value = function(...args) {
const key = JSON.stringify(args)
if(cache[key]) {
return cache[key]
} else {
cache[key] = old.call(this,...args)
return cache[key]
}
}
return descriptor
}
class Util {
@cacheable
add(a,b) {
console.log("add",a,b)
return a + b
}
}
const util = new Util()
util.add(1,2)
util.add(1,2)
util.add(1,2)
// 只有第一次运行会打印出 add 1 2
同样的,我们也可以实现属性上的装饰器,比如实现一个类似java包装类
class Integer{
constructor(num){
this.value = num
}
display() {
console.log(`this is a boxing value , the value is ${this.value}`)
}
}
function boxing(target,key,descriptor) {
let v = descriptor.initializer && descriptor.initializer.call(this);
v= new Integer(v)
return {
enumerable: true,
configurable: true,
get: function() {
return v;
},
set: function(c) {
v = new Integer(c);
}
}
}
class Number {
@boxing
a=2
}
let number = new Number()
number.a.display()
// this is a boxing value , the value is 2
number.a = 4
number.a.display()
// this is a boxing value , the value is 4
这样,我们就实现了一个简单的包装Integer。
如果我们每次赋值的时候,都希望检查一下数据类型,可以这样做
function check(type) {
return function(target,key,descriptor) {
let v = descriptor.initializer && descriptor.initializer.call(this)
return {
enumerable: true,
configurable: true,
get: function() {
return v;
},
set: function(c) {
if(typeof c !== type) {
throw new Error("error type")
}
v = c
}
}
}
}
¶第三方库
core-decorators.js是一个第三方模块,提供了一些常见的装饰器
比如:
// @autobind
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person;
// true
// @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]
还有更多不再阐述。
¶总结
上文只是简单对装饰器模式做了基本原理的解释和简单demo,具体在实战中如何应用,还需要很多其他的经验,比如多个装饰器嵌套如何使用,如何利用proxy实现装饰器,在上文中都没提及,在接下来的博文里,会完整的实现一个具有实际意义的装饰器例子。