原文连接:Nealyang/personalBloghtml
ES6 已经没必要在过多介绍,在 ES6 以前,装饰器可能并无那么重要,由于你只须要加一层 wrapper 就行了,可是如今,因为语法糖 class 的出现,当咱们想要去在多个类之间共享或者扩展一些方法的时候,代码会变得错综复杂,难以维护,而这,也正式咱们 Decorator 的用武之地。git
关于 Object.defineProperty 简单的说,就是该方法能够精准的添加和修改对象的属性github
Object.defineProperty(obj,prop,descriptor)
segmentfault
该方法返回被传递给函数的对象bash
在ES6中,因为 Symbol类型的特殊性,用Symbol类型的值来作对象的key与常规的定义或修改不一样,而Object.defineProperty 是定义key为Symbol的属性的方法之一。babel
经过赋值操做添加的普通属性是可枚举的,可以在属性枚举期间呈现出来(for...in 或 Object.keys 方法), 这些属性的值能够被改变,也能够被删除。这个方法容许修改默认的额外选项(或配置)。默认状况下,使用 Object.defineProperty() 添加的属性值是不可修改的app
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具备值的属性,该值多是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是二者。函数
数据描述符和存取描述符均具备如下可选键值:ui
configurablethis
当且仅当该属性的 configurable 为 true 时,该属性描述符才可以被改变,同时该属性也能从对应的对象上被删除。默认为 false
enumerable
当且仅当该属性的enumerable为true时,该属性才可以出如今对象的枚举属性中。默认为 false。
数据描述符同时具备如下可选键值:
value
该属性对应的值。能够是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
存取描述符同时具备如下可选键值:
get
一个给属性提供 getter 的方法,若是没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,可是会传入this对象(因为继承关系,这里的this并不必定是定义该属性的对象)。默认为 undefined。
set
一个给属性提供 setter 的方法,若是没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受惟一参数,即该属性新的参数值。默认为 undefined。
若是一个描述符不具备value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。若是一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常
更多使用实例和介绍,参看:MDN
在看Decorator以前,咱们先看下装饰者模式的使用,咱们都知道,装饰者模式可以在不改变对象自身基础上,在程序运行期间给对象添加指责。特色就是不影响以前对象的特性,而新增额外的职责功能。
like...this:
这段比较简单,直接看代码吧:
let Monkey = function () {}
Monkey.prototype.say = function () {
console.log('目前我只是个野猴子');
}
let TensionMonkey = function (monkey) {
this.monkey = monkey;
}
TensionMonkey.prototype.say = function () {
this.monkey.say();
console.log('带上紧箍咒,我就要忘记世间烦恼!');
}
let monkey = new TensionMonkey(new Monkey());
monkey.say();
复制代码
执行结果:
Decorator其实就是一个语法糖,背后其实就是利用es5的Object.defineProperty(target,name,descriptor)
,了解Object.defineProperty请移步这个连接:MDN文档
其背后原理大体以下:
class Monkey{
say(){
console.log('目前,我只是个野猴子');
}
}
复制代码
执行上面的代码,大体代码以下:
Object.defineProperty(Monkey.prototype,'say',{
value:function(){console.log('目前,我只是个野猴子')},
enumerable:false,
configurable:true,
writable:true
})
复制代码
若是咱们利用装饰器来修饰他
class Monkey{
@readonly
say(){console.log('如今我是只读的了')}
}
复制代码
在这种装饰器的属性,会在Object.defineProperty为Monkey.prototype注册say属性以前,执行如下代码:
let descriptor = {
value:specifiedFunction,
enumerable:false,
configurable:true,
writeable:true
};
descriptor = readonly(Monkey.prototype,'say',descriptor)||descriptor;
Object.defineProperty(Monkey.prototype,'say',descriptor);
复制代码
从上面的伪代码咱们能够看出,Decorator只是在Object.defineProperty为Monkey.prototype注册属性以前,执行了一个装饰函数,其属于一个类对Object.defineProperty的拦截。因此它和Object.defineProperty具备一致的形参:
下面看下简单的使用
@name
class Person{
sayHello(){
console.log(`hello ,my name is ${this.name}`)
}
}
function name(constructor) {
return class extends constructor{
name="Nealyang"
}
}
new Person().sayHello()
//hello ,my name is Nealyang
复制代码
@name
@seal
class Person {
sayHello() {
console.log(`hello ,my name is ${this.name}`)
}
}
function name(constructor) {
Object.defineProperty(constructor.prototype,'name',{
value:'一凨'
})
}
new Person().sayHello()
//若修改一个属性
function seal(constructor) {
let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHello')
Object.defineProperty(constructor.prototype, 'sayHello', {
...descriptor,
writable: false
})
}
new Person().sayHello = 1;// Cannot assign to read only property 'sayHello' of object '#<Person>'
复制代码
上面说到mixin,那么我就来模拟一个mixin吧
class A {
run() {
console.log('我会跑步!')
}
}
class B {
jump() {
console.log('我会跳!')
}
}
@mixin(A, B)
class C {}
function mixin(...args) {
return function (constructor) {
for (const arg of args) {
for (let key of Object.getOwnPropertyNames(arg.prototype)) {
if (key === 'constructor') continue;
Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key));
}
}
}
}
let c = new C();
c.jump();
c.run();
// 我会跳!
// 我会跑步!
复制代码
截止目前我們貌似写了很是多的代码了,对。。。这篇,为了完全搞投Decorator,这。。。只是开始。。。
这类的装饰器的写法应该就是咱们最为熟知了,会接受三个参数:
首先,咱们明确下静态成员和实例成员的区别
class Model{
//实例成员
method1(){}
method2 = ()=>{}
// 靜態成員
static method3(){}
static method4 = ()=>{}
}
复制代码
method1 和method2 是实例成员,可是method1存在于prototype上,method2只有实例化对象之后才有。
method3和method4是静态成员,二者的区别在因而否可枚举描述符的设置,咱们经过babel转码能够看到:
上述代码比较乱,简单的能够理解为:
function Model () {
// 成员仅在实例化时赋值
this.method2 = function () {}
}
// 成员被定义在原型链上
Object.defineProperty(Model.prototype, 'method1', {
value: function () {},
writable: true,
enumerable: false, // 设置不可被枚举
configurable: true
})
// 成员被定义在构造函数上,且是默认的可被枚举
Model.method4 = function () {}
// 成员被定义在构造函数上
Object.defineProperty(Model, 'method3', {
value: function () {},
writable: true,
enumerable: false, // 设置不可被枚举
configurable: true
})
复制代码
能够看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,因此这就是为何在针对Property Decorator不传递第三个参数的缘由,至于为何静态成员也没有传递descriptor,目前没有找到合理的解释,可是若是明确的要使用,是能够手动获取的。
就像上述的示例,咱们针对四个成员都添加了装饰器之后,method1和method2第一个参数就是Model.prototype,而method3和method4的第一个参数就是Model。
class Model {
// 实例成员
@instance
method1 () {}
@instance
method2 = () => {}
// 静态成员
@static
static method3 () {}
@static
static method4 = () => {}
}
function instance(target) {
console.log(target.constructor === Model)
}
function static(target) {
console.log(target === Model)
}
复制代码
class Model {
@log1
getData1() {}
@log2
getData2() {}
}
// 方案一,返回新的value描述符
function log1(tag, name, descriptor) {
return {
...descriptor,
value(...args) {
let start = new Date().valueOf()
try {
return descriptor.value.apply(this, args)
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
}
}
// 方案2、修改现有描述符
function log2(tag, name, descriptor) {
let func = descriptor.value // 先获取以前的函数
// 修改对应的value
descriptor.value = function (...args) {
let start = new Date().valueOf()
try {
return func.apply(this, args)
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
}
复制代码
get
set
前缀函数了,用于控制属性的赋值、取值操做,在使用上和函数装饰器没有任何区别class Modal {
_name = 'Niko'
@prefix
get name() { return this._name }
}
function prefix(target, name, descriptor) {
return {
...descriptor,
get () {
return `wrap_${this._name}`
}
}
}
console.log(new Modal().name) // wrap_Niko
复制代码
class Modal {
@prefix
static name1 = 'Niko'
}
function prefix(target, name) {
let descriptor = Object.getOwnPropertyDescriptor(target, name)
Object.defineProperty(target, name, {
...descriptor,
value: `wrap_${descriptor.value}`
})
}
console.log(Modal.name1) // wrap_Niko
复制代码
对于一个实例的属性,则没有直接修改的方案,不过咱们能够结合着一些其余装饰器来曲线救国。
好比,咱们有一个类,会传入姓名和年龄做为初始化的参数,而后咱们要针对这两个参数设置对应的格式校验
const validateConf = {} // 存储校验信息
@validator
class Person {
@validate('string')
name
@validate('number')
age
constructor(name, age) {
this.name = name
this.age = age
}
}
function validator(constructor) {
return class extends constructor {
constructor(...args) {
super(...args)
// 遍历全部的校验信息进行验证
for (let [key, type] of Object.entries(validateConf)) {
if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
}
}
}
}
function validate(type) {
return function (target, name, descriptor) {
// 向全局对象中传入要校验的属性名及类型
validateConf[name] = type
}
}
new Person('Niko', '18') // throw new error: [age must be number]
复制代码
const parseConf = {}
class Modal {
@parseFunc
addOne(@parse('number') num) {
return num + 1
}
}
// 在函数调用前执行格式化操做
function parseFunc (target, name, descriptor) {
return {
...descriptor,
value (...arg) {
// 获取格式化配置
for (let [index, type] of parseConf) {
switch (type) {
case 'number': arg[index] = Number(arg[index]) break
case 'string': arg[index] = String(arg[index]) break
case 'boolean': arg[index] = String(arg[index]) === 'true' break
}
return descriptor.value.apply(this, arg)
}
}
}
}
// 向全局对象中添加对应的格式化信息
function parse(type) {
return function (target, name, index) {
parseConf[index] = type
}
}
console.log(new Modal().addOne('10')) // 11
复制代码
为一个方法添加 log 函数,检查输入的参数
let log = type => {
return (target,name,decorator) => {
const method = decorator.value;
console.log(method);
decorator.value = (...args) => {
console.info(`${type} 正在进行:${name}(${args}) = ?`);
let result;
try{
result = method.apply(target,args);
console.info(`(${type}) 成功 : ${name}(${args}) => ${result}`);
}catch(err){
console.error(`(${type}) 失败: ${name}(${args}) => ${err}`);
}
return result;
}
}
}
class Math {
@log('add')
add(a, b) {
return a + b;
}
}
const math = new Math();
// (add) 成功 : add(2,4) => 6
math.add(2, 4);
复制代码
用于统计方法执行的时间:
function time(prefix) {
let count = 0;
return function handleDescriptor(target, key, descriptor) {
const fn = descriptor.value;
if (prefix == null) {
prefix = `${target.constructor.name}.${key}`;
}
if (typeof fn !== 'function') {
throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
}
return {
...descriptor,
value() {
const label = `${prefix}-${count}`;
count++;
console.time(label);
try {
return fn.apply(this, arguments);
} finally {
console.timeEnd(label);
}
}
}
}
}
复制代码
对执行的方法进行防抖处理
class Toggle extends React.Component {
@debounce(500, true)
handleClick() {
console.log('toggle')
}
render() {
return (
<button onClick={this.handleClick}>
button
</button>
);
}
}
function _debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
function debounce(wait, immediate) {
return function handleDescriptor(target, key, descriptor) {
const callback = descriptor.value;
if (typeof callback !== 'function') {
throw new SyntaxError('Only functions can be debounced');
}
var fn = _debounce(callback, wait, immediate)
return {
...descriptor,
value() {
fn()
}
};
}
}
复制代码
更多关于 core-decorators 的例子后面再 Nealyang/PersonalBlog中补充,再加注释说明。