Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 中的装饰器语法 #50

Open
zhangxiang958 opened this issue Dec 5, 2019 · 0 comments
Open

JavaScript 中的装饰器语法 #50

zhangxiang958 opened this issue Dec 5, 2019 · 0 comments

Comments

@zhangxiang958
Copy link
Owner

什么是装饰器(Decorator)?

在设计模式中,装饰器是指在不改变原有类的成员方法下,通过一个包装对象来动态扩展原对象的功能。用通俗的话语来讲,它就像是一个画框,无论你是什么样的画,装上了画框就有了保护画的功能与挂上墙壁的功能。

而在其他很多语言包括 Java,Python,都有装饰器的语法,形如:

class Person {
	@readonly
    walk() {}
}

可以看到,通过 @ 这个字符,使用了 readonly 装饰器,非常方便地使得 Person 类的 walk 方法变成了一个只读的方法。而在 ES7 语法中,也同样拥有类似的语法,它不仅可以作用于类的成员上,还可以作用于类本身上面:

@testable
class Person {
    constructor(){}
}

function testable(target) {
    target.test = true;
}

这里例子中直接利用了 testable 这一个装饰器,给 Person 这个类加上了一个 test 的属性。

可以看到装饰器可以动态地为 Person 这个类加上了一些特殊的标识,而且装饰器可以复用到很多不同的类中。

装饰器的原理是什么?

实际上,在 EcmaScript 中,装饰器语法是依赖于 Object.defineProperty 这个 API 的,甚至可以说装饰器是它的一个语法糖。

装饰器是代码编译期间对类或类成员发生改变,并不是运行时,我们来看一下 babel 转义出来的代码:

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
	// 接收装饰器数组 decorators
    var desc = {};
    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;
}

// 调用方式
_applyDecoratedDescriptor(_class2.prototype, 'key', [decorator], Object.getOwnPropertyDescriptor(_class2.prototype, 'key'), _class2.prototype)

如果这个装饰器是直接作用在类上面的,那么就像之前所说的那样,翻译代码如:

class _Person {
    constructor(){}
}

const Person = decorator(_Person) || _Person;

从上面的代码可以看出 decorator 确实是在代码编译阶段就执行的,并且翻译后依赖于 Object.defineProperty 这个 API,实现了对于原成员的重新定义与包装。

装饰器本质上是一个函数,它接收的参数形式都是固定的,也就是 Object.defineProperty 函数的参数,第一个参数是作用的对象,第二个参数是需要重新定义的对应的键名,第三个参数是属性描述对象,对象包括 writable, configurable, enumerable 等参数,详细的参数可以看 MDN 文档,这里不再累述。

装饰器怎么用?

装饰器可以作用在类以及类的成员上面。如果直接作用在类上面,那么装饰器函数中只有一个参数,如下:

@color('white')
class Cat {
    constructor(name) {
        this.name = name;
    }
}

function color(style) {
    return function (target) {
        target.prototype.color = style;
    }
}

const cat = new Cat('tom');
console.log(cat.color); // 'white'

color 函数的返回值是一个函数,返回的这个函数会在编译阶段就被执行,执行后函数传入的 target 也就是这里的 Cat 类会被添加上一个 color 属性在原型上,值为 'white'。

如果是作用在类的成员上的话,那么它回调函数中的参数就会如同 Object.defineProperty 函数一样,如下:

class Person {
    name = createName();
    constructor() {}

    @withWho('girlFriend')
    walk() {
        console.log('I \'m walking.');
    }
}

function withWho (name) {
    return function (target, key, descriptor) {
        console.log(`with ${name}`);
        console.log(target);
        console.log(key);
        console.log(descriptor);
        const func = descriptor.value; // 通过 value 获取
        const editedFunc = function () {
            console.log(`with ${name}`);
            return func();
        }
        descriptor.value = editedFunc;
    }
}

const jack = new Person();
jack.walk();

// with girlFriend
// I 'm walking

上面的例子通过 withWho 这个装饰器,实现了对 Person 类的 walk 方法的包裹,后续如果调用 walk 方法,都相当于执行装饰器包裹后的 editedFunc 函数的逻辑。

使用场景有哪些?

装饰器的作用很多,可以高度复用于很多类上与函数上,并且具有注释的功能,像文章最开始的代码,一看就知道 Person 类是具有可供测试的特性的。下面,我将列出一些常用的修饰器函数:

有哪些常见的装饰器?

readonly

function readonly(target, key, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

autobind

首先 autobind 分为 4 种情况:

  1. 直接读取对应 autobind 的方法,即方法定义在 Class Father 上面,直接通过 Father.prototype.xxx 来读取,此时直接返回对应的方法,不做 bind。
  2. autobind 作用的方法定义在 Father 类上面,然后 Son 子类继承于父类即 class Son extends Father,然后直接通过 Son.prototype.xxx 来读取方法,此时 this.constructor(Son) 是不同于原本的 construtor (Father),直接返回函数。
  3. Autobind 作用的函数在 this 对象的原型链上,此时不能去修改 descriptor,因为这是公用的 class 上面的 descriptor,所以需要每个对象重新绑定一次,为了避免重复绑定造成内存泄漏,所以这里使用 weekset 来存储已经绑定过的函数。
  4. 除了 1,2,3 种情况,就是直接在实例对象上面读取,那么直接就使用 bind 绑定对应的函数,使用 Object.defineProperty 来重新定义对应对象的 key。
let store;
function getBoundSuper(context, fn) {
	if (typeof WeekSet === 'undefined') {
        throw new Error(`WeekSet must suppoerted`);
	}
	
	if(!store) {
        store = new WeekSet();
	}
	
    if (!store.has(context)) {
        store.set(context, new WeekSet());
    }
    
    const contextStore = store.get(context);
    
    if (!contextStore.has(fn)) {
        contextStore.set(fn, fn.bind(context));
    }
    
    return contextStore.get(fn);
}


function autobind(target, key, descriptor) {
	const { constructor } = target;
    const { value, configurable, enumerable } = descriptor;
    const func = value;
    if (typeof func !== 'function') {
        throw new SyntaxError('@autobind can only use on functions');
    }
    
    return {
        configurable,
        enumrable,
        get() {
            if (this === target) {
                return this;
            }
            
            if (this.constructor !== constructor && Object.getPrototypeOf(this).constructor === constructor) {
                return func;
            }
            
            if (this.constructor !== constructor && key in this.constructor.prototype) {
                return getBoundSuper(this, func);
            }
            
            const bindFunc = value.bind(this);
            Object.defineProperty(target, key, {
                configurable,
                enumerable,
                value: bindFunc,
                writable: true
            });
            return bindFunc;
        }
    };
}

deprecate

function deprecate(...args) {
	const [msg = 'This function will be deprecate in future.'] = args;
    return function (target, key, descriptor) {
        if (typeof descriptor.value !== 'function') {
            throw new SyntaxError('only functions can be marked as deprecated.');
        }
        
        const deprecateSignal = `${target.constructor.name}#${key}`;
        return {
            ...descriptor,
            value: function deprecateWrap() {
                console.warn(`[DEPRECATION]: ${deprecateSignal} ${msg}`);
                return descriptor.value.apply(this, arguments);
            }
        };
    }
}

lazyInitialize

function lazyInitialize (target, key, descriptor) {
	const { configurable, enumerable, initialize, value } = descriptor;
   	
   	Object.defineProperty(target, key, {
   		get() {
          	const ret = initiallize ? initialize.call(this): value;
            
            Object.defineProperty(target, key, {
                configurable,
                enumerable,
                value: ret,
                writable: true
            });
            
            return ret;
   		},
        configurable,
        enumerable
   	});
}

当使用到该属性的时候,才去初始化对应属性的值,并且只初始化一次。只有当 lazyInitialize 修饰的是类的属性的时候,才会有 initialize 这个 descriptor 属性。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant