ES7 Decorator(修饰器) 简单入门
前言
本文有引用阮一峰的例子,侵删。
一、Decorator 是什么
很多语言都有对应的Decorator(修饰器),用来修改原类/方法的功能。比如Java的Annotationy注解也算是修饰器。
不改变原代码内容逻辑的前提下丰富原有功能
class App {
get state(){
return 666
}
render(){
console.log("this is App's render func,state is "+ this.state);
}
}
想在App render的时候前后插入自定义的文字,正常的想法是这样的
class App {
get state(){
return 666
}
render(){
console.log("wrap log begin")
console.log("this is App's render func,state is "+ this.state);
console.log("wrap log end")
}
}
如果我有A,B、C App的render都要前后输出自定义文字怎么办?每个都改?万一哪天文字输出方式改变,一个个改吗?这时候修饰器作用就来了
const logWrapper = targetClass =>{
const orignRender = targetClass.prototype.render;
targetClass.prototype.render = function(){
console.log("wrap log begin")
orignRender.apply(this);//防止this指向改变了
console.log("wrap log end")
}
return targetClass;
}
@logWrapper
class App {
get state(){
return 666
}
render(){
console.log("this is App's render func,state is "+ this.state);
}
}
new App().render();
//输出:
//wrap log begin
//this is App's render func,state is 666
//wrap log end
二、为什么要用Decorator
对JavaScript来说应该算是一个语法糖,高阶函数的使用方式。以上例子在不使用Decorator的方式是:
class App {
get state(){
return 666
}
render(){
console.log("this is App's render func,state is "+ this.state);
}
}
const wrapLogApp = logWrapper(App);
new wrapLogApp().render();
//输出:
//wrap log begin
//this is App's render func,state is 666
//wrap log end
优点就是看起来直观一点、容易理解、省略了临时变量命名。如果你觉得优点不够明显,那看下的React+Redux例子:
- 多参数修饰器
class ReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(ReactComponent);
村里通电用上修饰器后:
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
- 多参数、多重修饰器:
class ReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(logWrapper(ReactComponent));
修饰器加成:
@connect(mapStateToProps, mapDispatchToProps)
@logWrapper
export default class MyReactComponent extends React.Component {}
是不是更好理解了?
三、Decorator 的实现与用法
3.1 使用方式
参数
- 单参数
修饰方法的第一个参数必定为要修饰的类/方法
const logWrapper = function(targetClass){
//do something
}
- 多参数
除了默认的参数,也可能有修饰方法自己的参数。这时候这个方法需要返回一个方法作为新的修饰方法
const logWrapper = (logText) =>{
return targetClass =>{
targetClass.render = () =>{console.log(logText)}
}
}
//使用方式
@logWrapper("Hello World")
class App{
//注意,这里没有render方法,通过注入一个render方法
}
new App().render();//输出"Hello World"
返回值
其实上面讲的差不多了,具体的执行步骤大概是这样的
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
修饰器不一定要返回对象,但如果不返回新对象,那么要对该对象属性进行修改。个人还是推荐有个返回值,也比较好理解,反正你又不亏。
多层级
当一个被修饰的类有多个修饰符,我这里把分成两个阶段:初始化、运行时。
const countWrapper = count =>{
console.log("修饰初始化:获取修饰方法",count);
return target => {
console.log("修饰运行时:修饰方法开始修饰类/方法",count);
return target;
}
}
@countWrapper(1)
@countWrapper(2)
class App {
}
//输出
// 修饰初始化:获取修饰方法 1
// 修饰初始化:获取修饰方法 2
// 修饰运行时:修饰方法开始修饰类/方法 2
// 修饰运行时:修饰方法开始修饰类/方法 1
- 修饰初始化
代码在声明变量的时候,由上而下执行代码块,因此获取修饰方法按照顺序执行:
countWrapper(1)
countWrapper(2)
获得对应的修饰方法
- 修饰运行时
因为countWrapper(1)需要接受一个参数来修饰,但是参数还没形成,所以等下一个countWrapper(2)修饰完的对象返回值当做1的参数。说的有点绕,拆成非修饰器的实现方式如下:
countWrapper(1)(countWrapper(2)(App))
总之修饰方法对类/方法进行修饰的原则是谁靠的近,谁先执行。
3.1 类的修饰
const uuidWrapper = target => {//给目标类加一个静态变量uuid
target.uuid = uuid();
}
@uuidWrapper
class App {
}
3.2 webpack + Babel
现在很多浏览器还不支持修饰器,用上一些打包工具的时候就可以随心所欲使用修饰器了。
在已经支持ES6语法的前提下:
- 安装依赖
npm install babel-plugin-transform-decorators-legacy --save-dev
- 修改babel配置
.babelrc
{
// 新增一个decorator 插件
"plugins": ['transform-decorators-legacy']
}
四、注意点
4.1 修饰器不能用于函数
修饰器只能修饰类、类方法。如果直接用于函数会有函数提升的问题,类方法与函数的区别是在类内部不会有函数提升。因此直接用于函数的时候babel插件处理会有错误提示。
关于函数提升:
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++;
};
- 解决方法:如果一定要修饰函数,可以采用高阶函数的形式直接执行
var counter = 0;
var add = function () {
counter++;
};
function foo() {
}
const addFun = add(foo)