装饰器的简单实现(下)

Author Avatar
zzz1220 7月 03, 2019
  • 在其它设备中阅读本文章
👉👉本文共1.5k字📘 读完共需6分钟⌚

上文介绍了装饰模式,装饰器语法,今天继续说一下怎么用javascript实现自定义装饰器。

启用装饰器语法

目前decorator语法只是题案,想要使用装饰器,需要安装babelwebpack并结合babel/plugin-proposal-decorators 插件。

关于如何配置请参考babel插件的配置文档。

或者你也可以直接到 https://babeljs.io/repl/ 这个地址编写代码,它提供了一个在线的repl环境,可以直接运行es6代码,记得勾选左侧的Experimental来启用装饰器语法。

🔑 es2015,es2016,es2017es6之间的关系:

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

有了装饰器,之前那些烦人的样板函数就可以用装饰器代替了,比如我们用reduxreact时,需要把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高级程序设计关于 definePropertydefineProperties的介绍,这两个方法就是用来定义和修改对象的内部属性。
举一个常见的例子,在每次函数执行前后,把函数的参数和结果打印出来

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实现装饰器,在上文中都没提及,在接下来的博文里,会完整的实现一个具有实际意义的装饰器例子。