rwson

rwson

一个前端开发

decorator的学习

随着前端技术的发展,越来越多人把ES6、7用在日常开发中,在ES7中除了广为人知的async/await之外,还有一大特性 — decorator(装饰器)。

在之前原生javascript设计模式中的一篇文章(javascript装饰者模式)中说道: js装饰者模式可以把一个对象(类/函数)透明地包装在另外一个对象上,完成对被装饰者添加一些新功能的作用。简单的说我们可以在不修改类/函数内部代码的情况下,来达到给类/函数加入一些新功能。

装饰器可以作用于类或者类的成员属性/方法上,下面我们通过两段代码来分别解释:

//  作用于类

/**
 * 需求:
 * 封装4个方法,分别实现加减乘除四个功能
 * 通过装饰器给类添加这四个方法,并且可指定是否作为类的静态方法添加
 **/
 
//  加减乘除的实现

function add() {
    return [].slice.call(arguments).reduce((a, b) => a + b);
}

function reduce() {
    return [].slice.call(arguments).reduce((a, b) => a - b);
}

function mul() {
    return [].slice.call(arguments).reduce((a, b) => a * b);
}

function div() {
    return [].slice.call(arguments).reduce((a, b) => a / b);
}


@bindCal(add, true)
@bindCal(reduce, true)
@bindCal(mul, false)
@bindCal(div, false)
class MyMath {

}

上面我们完成了需求中的四个函数的封装以及对MyMath类应用了装饰器,下面我们就来实现这个装饰器:

/**
 * @param    {Function}   method    指向需要被添加方法的指针
 * @param    {String}     isStatic  是否添加为静态属性,默认为true
 * @return   {Function}
 */
function bindCal(method, isStatic = true) {
    //  获取到方法名
    const {name} = method;
    //  当作用于类上时,会返回一个匿名函数,将类的构造函数作为第一个参数
    return function(target) {
        //  获取类的原型
        const {prototype} = target;
        //  添加为原型属性(非静态属性)
        if (!isStatic) {
            //  检测要添加的属性是否存在
            if (prototype[name]) {
                throw `${prototype.name}.${name} already exits!`;
            } else {
                //  利用ES5中的Object.defineProperty来添加相关属性
                Object.defineProperty(target.prototype, name, {
                    value: method
                });
            }
        } else {
            //  添加为静态属性
            //  检测要添加的属性是否存在
            if (target[name]) {
                throw `${target}.${name} already exits!`;
            }
            //  利用ES5中的Object.defineProperty来添加相关属性
            Object.defineProperty(target, name, {
                value: method
            });
        }
        //  返回类的构造器
        return target;
    }
}

我们把add和reduce作为静态方法添加给MyMath类,把mul和div作为原型方法给了MyMath类,所以调用应该看起来像下面的样子:

const math = new MyMath();

console.log(MyMath.add(1, 2, 3, 4));        //  10
console.log(MyMath.reduce(1, 2, 3, 4));     //  -8


console.log(math.mul(1, 2, 3, 4));          //  24
console.log(math.div(10000, 1000, 10, 5));  //  0.2

上面就是我们在类上应用装饰器的一个例子,下面再一起看下如果在成员属性/方法上应用构造器的例子:

/**
 * 需求:
 * 实现在类中可以冻结成员属性(外部无法修改,只读)
 * 实现修改类中成员方法中的this指向
 **/


//  实现一个类,并且对其成员属性/方法应用装饰器、定义一个对象,作为成员方法中的this指向


const obj = {
    name: "rwson",
    age: 24,
    sex: "male",
    job: "web developer"
};

class Context {

    constructor() {
    
    }
    
    @readonly
    version = "1.0.0";
    
    @bindContext(obj)
    showContext() {
        console.log(this);
    }

} 

上面我们完成了对装饰器应用过程,下面一起看下readonly和bindContext两个装饰器的实现:

/**
 * @param    {Object}   target    当前类的prototype
 * @param    {String}   key       将要被装饰的属性名
 * @param    {Object}   decorator ES5中Object.defineProperty的最后一个参数
 * @return   {Object}   decorator
 */
function readonly(target, key, decorator) {
	decorator.configurable = false;
	decorator.enumerable = false;
	decorator.writable = false;
	decorator.value = decorator.value;
	return decorator;
}

/**
 * @param    {Object}   context    需要绑定成this的对象
 * @return   {Function}
 */
function bindContext(context) {
	/**
	 * @param    {Object}   target    当前类的prototype
	 * @param    {String}   key       将要被装饰的属性名
	 * @param    {Object}   decorator ES5中Object.defineProperty的最后一个参数
	 * @return   {Object}   decorator
	 */
    return function(target, key, decorator) {
    	if (typeof context === "undefined") {
    		context = target;
    	}
        decorator.value = decorator.value.bind(context);
        return decorator;
    }
}

好了上面就是我们的两个作用于成员属性/方法上的装饰器,下面一起来看下简单的调用吧:

const context = new Context();

context.showContext();          //  打印出刚才定义的obj对象
context.version = "1.1.0";      //  抛出异常 Uncaught TypeError: Cannot assign to read only property 'version' of object '#<Context>'

好了,上面就装饰器的几种用法和实现,我们可能会发现刚才在定义装饰器函数的时候,当该装饰器作用于类上的时候返回出的匿名函数都是一个参数;而作用于成员属性或者成员方法上的,都是三个参数,这又是为啥呢?

一起来分析下编译后的代码:

/**
 * @param    {Object}   			target     当前类的prototype
 * @param    {String}   			property   将要被装饰的属性名
 * @param    {Array.<Function>}   	decorators 装饰器函数列表
 * @param    {Object}   			descriptor ES5中Object.defineProperty的最后一个参数
 * @param    {[type]}   context    [description]
 * @return   {[type]}              [description]
 * @private
 */
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {

	//	属性对象
	var desc = {};

	//	Object["keys"]	-> ["value", "writable", "enumerable", "configurable"]
	//	把ES5中Object.defineProperty的最后一个参数的属性变成外部指定的
	Object['ke' + 'ys'](descriptor).forEach(function (key) {
		desc[key] = descriptor[key];
	});
	desc.enumerable = !!desc.enumerable;
	desc.configurable = !!desc.configurable;

	if ('value' in desc || desc.initializer) {
		desc.writable = true;
	}

	desc = decorators.slice().reverse().reduce(function (desc, decorator) {
		return decorator(target, property, desc) || desc;
	}, desc);

	if (context && desc.initializer !== void 0) {
		desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
		desc.initializer = undefined;
	}

	//	利用Object.defineProperty定义属性
	if (desc.initializer === void 0) {
		Object['define' + 'Property'](target, property, desc);
		desc = null;
	}

	//	返回当前属性对象
	return desc;
}

可以看出其实decorator最后也是通过Object.defineProperty实现的,所以参数和Object.defineProperty是一致的。

那日常开发中,我们可能需要借助babel来对带有decorator的代码进行编译,首先我们需要安装babel:

npm install babel -g

然后切换到项目目录运行:

npm install babel-plugin-transform-decorators-legacy --save-dev

然后创建.babelrc配置文件,在plugins选项中添加以下配置:

//  ...
"plugins": [
    "transform-decorators-legacy"
]
//  ...

最后我们就可以编译之前写的代码了:

babel decorator.js > decorator.es5.js