Obeta

TypeScript装饰器

JavaScript中的装饰器提案目前还在第二阶段寻求建议,还未进入实现标准,因此以后可能会有一些细节上的改变,但是基本的使用是一致的

可以查看 Github 上的 proposal-decorators1提案了解一下,其中还有大量的官方示例参考.

decorator有人叫装饰器,也有人叫修饰器,个人觉得其实都差不多,本文采用装饰器的说法,因为在其它语言中也这么称呼.Python 中的 Flask 框架就大量使用了装饰器,而 Java 中被称为 annotation,也就是注解器.不管怎么样这个东西的使用会令人非常愉悦,代码会简洁很多.~

介绍

装饰器其实就是一个函数,作用就是包裹原来的对象、类、类属性甚至类成员的参数进行内部处理,也是一种元编程模式的体现,感兴趣的同学可以阅读我之前写过一篇JavaScript 中的元编程

总之,用通俗的来说就是装饰器这段代码包裹源代码,对源代码就行修饰,下面例子是使用 ES5 语法写的装饰器:

// decorator 1
function logDecorator(wrapped) {
	return function(...args) {
		console.log('log start');
		const result = wrapped.apply(this, args);
		console.log('log end');
		return result;
	};
}

// source function
function doTask(message) {
	console.log('task: ', message);
}

const wrapped = logDecorator(doTask); // return new fucntion

doTask('decorator');
// task: decorator

wrapped('decorator');
// log start
// task: decorator
// log end

又或者使用的组合继承模式模仿类:

function Father(name) {
	this.name = name;
}
Father.prototype.loggerName = function() {
	console.log('name: ', this.name);
	return this.name;
};

function Mother(age) {
	this.age = age;
}
Mother.prototype.getAge = function() {
	return this.age;
};

function inherit(Father) {
	return function(Mother) {
		function children(name, age) {
			Father.call(this, name);
			Mother.call(this, age);
		}
		children.prototype = Object.create(
			Object.assign({}, Father.prototype, Mother.prototype)
		);
		children.prototype.constructor = children;
		return children;
	};
}

const Children = inherit(Father)(Mother);

const children = new Children('zzz', 0);
children.getAge(); // 0
children.loggerName(); // 'zzz'

logDecorator就是一个装饰器,虽然我们可以一直这么使用下去,但是代码中到处这类使用令人看起来非常困惑,因为看起来就像是一个普通函数,会造成阅读困难.

入门

以下例子需要使用 TypeScript 并在配置文件tsconfig.json中加入"experimentalDecorators": true,如果需要在 js 中使用,则需要安装babel-plugin-transform-decorators-legacy插件

上面例子中装饰器的使用让人难以维护和判断,因此提案中使用了一个特殊的语法符号@标志装饰器,比如:

@log
@immutable()
class Example {
	@timeLimit(2000)
	doTask() {
		// someting task
	}
}

以上使用了三个装饰器,其中log,immutable都是类装饰器,timeLimit是类成员装饰器,含义非常简单:

  • log: 类的访问日志
  • immutable: 类不可变,相当于使用了 Object.freeze 方法
  • timeLimit: 函数执行时间限制,超过既定时间报错

目前被装饰的东西只有以下几种:

  • 类成员(方法)
  • 类属性
  • 类成员参数

因为函数存在函数提升,因此无法对函数使用装饰器,可以参考使用上面的logDecoratorinherit实现类似装饰器功能的工厂函数.

针对上面四种装饰器接收参数并不都是相同的,个有差异,有些时候需要组合各个类型的装饰器实现一些复杂的功能,下面一个个的介绍.

对于装饰类的装饰器,被赋予了对类构造器constructor的访问权限:

function decorator(constructors) {
	// 可以这里修改构造器
}

@decorator
class Task {}

常见多用法是对类做一些扩展,比如一些deprecated,log等等,下面以deprecated为例:

function deprecated(constructors) {
	console.warn(
		`Deprecated: ${constructors.name} is deprecated and will be removed in a future version`
	);
}

@deprecated
class Task {}

const com = new Task(); // Deprecated: the Task is deprecated and will be removed in a future version

如果是一个需要传入参数的装饰器,那么需要写成二阶函数,也叫装饰器工厂:

function deprecated(message) {
	return constructors => {
		console.warn(
			`Deprecated: ${constructors.name} is deprecated and will be removed in a future version`
		);
		console.warn(message);
	};
}

@deprecated('Related issue at https://github.com/xxxxx/xxxxx')
class Task {}

const com = new Task();
// Deprecated: the Task is deprecated and will be removed in a future version
// Related issue at https://github.com/xxxxx/xxxxx

如果需要对类就行细化处理,比如获取参数、修改类等等就需要重载构造函数,也就是返回一个新的类或函数:

// 打印参数
function log(constructors) {
	return (...args) => {
		console.log(args);
		return new constructors(...args);
	};
}
@log
class People {
	constructor(name: string, age: number) {}
}
const com = new People('zzz', 18); // ['zzz', 18]

// 重载并修改类
function reset(constructors) {
	return class extends constructors {
		age = 0;
		name = 'nobody';
		constructor(address) {
			super();
			this.address = address;
		}
	};
}
@reset
class Son {
	constructor(name: string, age: number) {}
}
console.log(new Son('zzz', 18, 'chengdu')); // { age: 0, name: 'nobody', address: 'chengdu' }

// 自定义log头
function logMessage(message) {
	return constructors => {
		return (...args) => {
			console.log(message, args);
			return new constructors(...args);
		};
	};
}
@logMessage('Parent: ')
class Parent {
	constructor(name: string, age: number) {}
}
const com = new Parent('zzz', 18); // Parent: ['zzz', 18]

类成员

装饰器会在运行时当作函数被调用,接收以下三个值:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • name: 成员名称
  • descriptor: 成员属性描述符

如果装饰器返回一个值,它会被用作的属性描述符,关于属性描述符可以查看我这篇深入理解 javascript 对象文章

下面实现一个readonly装饰器:

function readonly(target, name: string, descriptor) {
	descriptor.writable = false;
	return descriptor;
}

class Task {
	@readonly
	private task() {}
}
const com = new Task();
com.task = 1; // TypeError

又或者通过劫持descriptor中的value来实现一些错误拦截:

function errorCatch(target, name: string, descriptor) {
	const original = descriptor.value;
	if (typeof original === 'function') {
		descriptor.value = function(...args) {
			try {
				const result = original.apply(this, args);
				return result;
			} catch (e) {
				console.warn('Decorator Catch Error: ', e);
				return null;
			}
		};
	}
	return descriptor;
}
class Task {
	@errorCatch
	private Task() {}
}

当然同类装饰器一样,可以实现一个装饰器工厂:

function errorCatch(message) {
	return function errorCatch(target, name: string, descriptor) {
		const original = descriptor.value;
		if (typeof original === 'function') {
			descriptor.value = function(...args) {
				console.log(message);
				try {
					const result = original.apply(this, args);
					return result;
				} catch (e) {
					console.warn('Decorator Catch Error: ', e);
					return null;
				}
			};
		}
		return descriptor;
	};
}
class Task {
	@errorCatch('Task')
	private Task() {}
}

类属性

装饰器接收以下两个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • name: 属性名称
function prop(target, name: string) {}

class Task {
	@prop
	private name: string = '';
}

由于目前 TypeScript 不支持传入属性描述符,因此此用法较少,估计还不够稳定

类成员参数

装饰器接收以下三个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • name: 当前成员名称
  • index: 参数所在位置索引

下面实现一个简单的参数验证例子:

const requireKey = new WeakMap();

// 参数验证记录
function required(target, name: string, index: number) {
	requireKey[target]
		? requireKey[target].push({
				name,
				index,
		  })
		: (requireKey[target] = [
				{
					name,
					index,
				},
		  ]);
}

// 类成员进行执行前验证
function validate(target, name: string, descriptor) {
	const orginal = descriptor.value;
	descriptor.value = function(...args) {
		const required =
			requireKey[target].filter(item => item.name === name) || [];

		for (let i = 0; i < required.length; i++) {
			if (required[i] && args[i] === undefined) {
				console.error(`function required params ${name}`);
				return;
			}
		}
		orginal.apply(this, args);
	};
	return descriptor;
}

class Task {
	@validate
	private task(@required name: string) {
		console.log('start task: ', name);
	}

	public runTask(name: string) {
		this.task(name);
	}
}
const task = new Task();
task.runTask('zzz'); // start task: zzz
task.runTask(); // error function required arg task

总结

以上例子大部分都可以在TypeScript Play上测试,装饰器非常灵活,可以实现很多奇奇怪怪的操作,这也是元编程的特性之一,对于喜欢 OOP 的人来说这是另一个值得等待的新事物,npm 里有很多关于装饰器的工具库,比如core-decorators提供了大量的基本装饰器

个人随笔记录,内容不保证完全正确,若需要转载,请注明作者和出处.