Decorator从原理到实践我一点都不虚

前言原文链接:[Nealyang/personalBlog]() ES6 已经不必在过多介绍,在 ES6 之前,装饰器可能并没有那么重要,因为你只需要加一层 wrapper 就好了,但是现在,由于语法糖 class 的出现,当我们想要去在多个类之间共享或者扩展一些方法的时候,代码会变得错综复杂,难以维护,而这,也正式我们 Decorator 的用武之地。 Object.defineProperty关于 Object.defineProperty 简单的说,就是该方法可以精准的添加和修改对象的属性 语法 Object.defineProperty(obj,prop,descriptor) ojb:要在其上定义属性的对象prop:要定义或修改的属性的名称descriptor:将被定义或修改的属性描述符该方法返回被传递给函数的对象 在ES6中,由于 Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而Object.defineProperty 是定义key为Symbol的属性的方法之一。通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来(for...in 或 Object.keys 方法), 这些属性的值可以被改变,也可以被删除。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的 属相描述符对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。 数据描述符和存取描述符均具有以下可选键值: configurable当且仅当该属性的 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: ...

April 23, 2019 · 5 min · jiezi

Python装饰器高级用法

在Python中,装饰器一般用来修饰函数,实现公共功能,达到代码复用的目的。在函数定义前加上@xxxx,然后函数就注入了某些行为,很神奇!然而,这只是语法糖而已。原文地址:https://python-book.readthedocs.io微信公众号:小菜学编程 (coding-fan)场景假设,有一些工作函数,用来对数据做不同的处理:def work_bar(data): passdef work_foo(data): pass我们想在函数调用前/后输出日志,怎么办?<!–more–>傻瓜解法logging.info(‘begin call work_bar’)work_bar(1)logging.info(‘call work_bar done’)如果有多处代码调用呢?想想就怕!函数包装傻瓜解法无非是有太多代码冗余,每次函数调用都要写一遍logging。可以把这部分冗余逻辑封装到一个新函数里:def smart_work_bar(data): logging.info(‘begin call: work_bar’) work_bar(data) logging.info(‘call doen: work_bar’)这样,每次调用smart_work_bar即可:smart_work_bar(1)# …smart_work_bar(some_data)通用闭包看上去挺完美……然而,当work_foo也有同样的需要时,还要再实现一遍smart_work_foo吗?这样显然不科学呀!别急,我们可以用闭包:def log_call(func): def proxy(*args, **kwargs): logging.info(‘begin call: {name}’.format(name=func.func_name)) result = func(*args, **kwargs) logging.info(‘call done: {name}’.format(name=func.func_name)) return result return proxy这个函数接收一个函数对象(被代理函数)作为参数,返回一个代理函数。调用代理函数时,先输出日志,然后调用被代理函数,调用完成后再输出日志,最后返回调用结果。这样,不就达到通用化的目的了吗?——对于任意被代理函数func,log_call均可轻松应对。smart_work_bar = log_call(work_bar)smart_work_foo = log_call(work_foo)smart_work_bar(1)smart_work_foo(1)# …smart_work_bar(some_data)smart_work_foo(some_data)第1行中,log_call接收参数work_bar,返回一个代理函数proxy,并赋给smart_work_bar。第4行中,调用smart_work_bar,也就是代理函数proxy,先输出日志,然后调用func也就是work_bar,最后再输出日志。注意到,代理函数中,func与传进去的work_bar对象紧紧关联在一起了,这就是闭包。再提一下,可以覆盖被代理函数名,以smart_为前缀取新名字还是显得有些累赘:work_bar = log_call(work_bar)work_foo = log_call(work_foo)work_bar(1)work_foo(1)语法糖先来看看以下代码:def work_bar(data): passwork_bar = log_call(work_bar)def work_foo(data): passwork_foo = log_call(work_foo)虽然代码没有什么冗余了,但是看是去还是不够直观。这时候,语法糖来了~~~@log_calldef work_bar(data): pass因此,注意一点(划重点啦),这里@log_call的作用只是:告诉Python编译器插入代码work_bar = log_call(work_bar)。求值装饰器先来猜猜装饰器eval_now有什么作用?def eval_now(func): return func()看上去好奇怪哦,没有定义代理函数,算装饰器吗?@eval_nowdef foo(): return 1print foo这段代码输出1,也就是对函数进行调用求值。那么到底有什么用呢?直接写foo = 1不行么?在这个简单的例子,这么写当然可以啦。来看一个更复杂的例子——初始化一个日志对象:# some other code before…# log formatformatter = logging.Formatter( ‘[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s’, ‘%Y-%m-%d %H:%M:%S’,)# stdout handlerstdout_handler = logging.StreamHandler(sys.stdout)stdout_handler.setFormatter(formatter)stdout_handler.setLevel(logging.DEBUG)# stderr handlerstderr_handler = logging.StreamHandler(sys.stderr)stderr_handler.setFormatter(formatter)stderr_handler.setLevel(logging.ERROR)# logger objectlogger = logging.Logger(name)logger.setLevel(logging.DEBUG)logger.addHandler(stdout_handler)logger.addHandler(stderr_handler)# again some other code after…用eval_now的方式:# some other code before…@eval_nowdef logger(): # log format formatter = logging.Formatter( ‘[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s’, ‘%Y-%m-%d %H:%M:%S’, ) # stdout handler stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) stdout_handler.setLevel(logging.DEBUG) # stderr handler stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setFormatter(formatter) stderr_handler.setLevel(logging.ERROR) # logger object logger = logging.Logger(name) logger.setLevel(logging.DEBUG) logger.addHandler(stdout_handler) logger.addHandler(stderr_handler) return logger# again some other code after…两段代码要达到的目的是一样的,但是后者显然更清晰,颇有代码块的风范。更重要的是,函数调用在局部名字空间完成初始化,避免临时变量(如formatter等)污染外部的名字空间(比如全局)。带参数装饰器定义一个装饰器,用于记录慢函数调用:def log_slow_call(func): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > 1: logging.warn(‘slow call: {name} in {seconds}s’.format( name=func.func_name, seconds=seconds, )) return result return proxy第3、5行分别在函数调用前后采样当前时间,第7行计算调用耗时,耗时大于一秒输出一条警告日志。@log_slow_calldef sleep_seconds(seconds): time.sleep(seconds)sleep_seconds(0.1) # 没有日志输出sleep_seconds(2) # 输出警告日志然而,阈值设置总是要视情况决定,不同的函数可能会设置不同的值。如果阈值有办法参数化就好了:def log_slow_call(func, threshold=1): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn(‘slow call: {name} in {seconds}s’.format( name=func.func_name, seconds=seconds, )) return result return proxy然而,@xxxx语法糖总是以被装饰函数为参数调用装饰器,也就是说没有机会传递threshold参数。怎么办呢?——用一个闭包封装threshold参数:def log_slow_call(threshold=1): def decorator(func): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn(‘slow call: {name} in {seconds}s’.format( name=func.func_name, seconds=seconds, )) return result return proxy return decorator@log_slow_call(threshold=0.5)def sleep_seconds(seconds): time.sleep(seconds)这样,log_slow_call(threshold=0.5)调用返回函数decorator,函数拥有闭包变量threshold,值为0.5。decorator再装饰sleep_seconds。采用默认阈值,函数调用还是不能省略:@log_slow_call()def sleep_seconds(seconds): time.sleep(seconds)处女座可能会对第一行这对括号感到不爽,那么可以这样改进:def log_slow_call(func=None, threshold=1): def decorator(func): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn(‘slow call: {name} in {seconds}s’.format( name=func.func_name, seconds=seconds, )) return result return proxy if func is None: return decorator else: return decorator(func)这种写法兼容两种不同的用法,用法A默认阈值(无调用);用法B自定义阈值(有调用)。# Case A@log_slow_calldef sleep_seconds(seconds): time.sleep(seconds)# Case B@log_slow_call(threshold=0.5)def sleep_seconds(seconds): time.sleep(seconds)用法A中,发生的事情是log_slow_call(sleep_seconds),也就是func参数是非空的,这是直接调decorator进行包装并返回(阈值是默认的)。用法B中,先发生的是log_slow_call(threshold=0.5),func参数为空,直接返回新的装饰器decorator,关联闭包变量threshold,值为0.5;然后,decorator再装饰函数sleep_seconds,即decorator(sleep_seconds)。注意到,此时threshold关联的值是0.5,完成定制化。你可能注意到了,这里最好使用关键字参数这种调用方式——使用位置参数会很丑陋:# Case B-@log_slow_call(None, 0.5)def sleep_seconds(seconds): time.sleep(seconds)当然了,函数调用尽量使用关键字参数是一种极佳实践,含义清晰,在参数很多的情况下更是如此。智能装饰器上节介绍的写法,嵌套层次较多,如果每个类似的装饰器都用这种方法实现,还是比较费劲的(脑子不够用),也比较容易出错。假设有一个智能装饰器smart_decorator,修饰装饰器log_slow_call,便可获得同样的能力。这样,log_slow_call定义将变得更清晰,实现起来也更省力啦:@smart_decoratordef log_slow_call(func, threshold=1): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn(‘slow call: {name} in {seconds}s’.format( name=func.func_name, seconds=seconds, )) return result return proxy脑洞开完,smart_decorator如何实现呢?其实也简单:def smart_decorator(decorator): def decorator_proxy(func=None, **kwargs): if func is not None: return decorator(func=func, **kwargs) def decorator_proxy(func): return decorator(func=func, **kwargs) return decorator_proxy return decorator_proxysmart_decorator实现了以后,设想就成立了!这时,log_slow_call,就是decorator_proxy(外层),关联的闭包变量decorator是本节最开始定义的log_slow_call(为了避免歧义,称为real_log_slow_call)。log_slow_call支持以下各种用法:# Case A@log_slow_calldef sleep_seconds(seconds): time.sleep(seconds)用法A中,执行的是decorator_proxy(sleep_seconds)(外层),func非空,kwargs为空;直接执行decorator(func=func, **kwargs),即real_log_slow_call(sleep_seconds),结果是关联默认参数的proxy。# Case B# Same to Case A@log_slow_call()def sleep_seconds(seconds): time.sleep(seconds)用法B中,先执行decorator_proxy(),func及kwargs均为空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(func, **kwargs),等价于real_log_slow_call(sleep_seconds),效果与用法A一致。# Case C@log_slow_call(threshold=0.5)def sleep_seconds(seconds): time.sleep(seconds)用法C中,先执行decorator_proxy(threshold=0.5),func为空但kwargs非空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(sleep_seconds, **kwargs),等价于real_log_slow_call(sleep_seconds, threshold=0.5),阈值实现自定义!订阅更新,获取更多学习资料,请关注我们的 微信公众号 : ...

February 24, 2019 · 2 min · jiezi

Vue框架TypeScript装饰器使用指南

关键词 装饰器 Decorator 元编程前言装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。本篇先从项目的宏观角度来总结一下Decorator如何组织。我会持续分享一些知识整理,如果文章对您有帮助记得点赞鼓励一下哦????~,也可以通过邮件方式联系我文章列表: https://juejin.im/user/5bc8b9… 邮箱地址: 595506910@qq.com目录主要的Decorator依赖vue-class-componentvuex-classvue-property-decoratorcore-decorators自定义Decorator示例哪些功能适合用Decorator实现Decorator实现小TipsSee also主要的Decorator依赖vue-cli3 默认支持Decorator, 年初重写了一个design库主要依赖官方和社区提供的Decorator来实现的组件。Decorator可以非侵入的装饰类、方法、属性,解耦业务逻辑和辅助功能逻辑。以下是主要的三方Decorator组件,有了这些组件常用的Vue特性就可以全部转成Decorator风格了。vue-class-component@Component 如果您在声明组件时更喜欢基于类的 API,则可以使用官方维护的 vue-class-component 装饰器实时计算computed属性, get computedMsg () {return ‘computed ’ + this.msg}生命周期钩子 mounted () {this.greet()}vuex-class让Vuex和Vue之间的绑定更清晰和可拓展@State@Getter@Action@Mutationvue-property-decorator这个组件完全依赖于vue-class-component.它具备以下几个属性:@Component (完全继承于vue-class-component)@Prop:父子组件之间值的传递@Emit:子组件向父组件传递@Model:双向绑定@Watch:观察的表达式变动@Provice:在组件嵌套层级过深时。父组件不便于向子组件传递数据。就把数据通过Provide传递下去。@Inject:然后子组件通过Inject来获取Mixins (在vue-class-component中定义);建议项目中只引用vue-property-decorator就可以了,避免@Component从vue-class-component和vue-property-decorator两个中随意引用。core-decorators@readonly@autobind : TSX 回调函数中的 this,类的方法默认是不会绑定 this 的,可以使用autobind装饰器@override总结一下主要就分成这三类:修饰类的:@Component、@autobind;修饰方法的:@Emit、@Watch、@readonly、@override;修饰属性的:@Prop、@readonly;以上引用方法等详系内容可查看官方文档。要想完整的发挥Decorator的价值就需要根据需要自定义一些装饰器。下面自定义部分就来实现一个记录日志功能的装饰器。自定义Decorator示例@Logger,1.Logger日志装饰器通常是修饰方法,Decorater则是在运行时就被触发了,日志记录是在方法被调用时触发,示例中通过自动发布事件实现调用时触发。2.为增加日志记录的灵活性,需要通过暴露钩子函数的方式来改变日志记录的内容。期望的日志格式{ “logId”:"", // 事件Id “input”:"", // 方法输入的内容 “output”:"", // 方法输出的内容 “custom”:"" // 自定义的日志内容}实现export function Logger(logId?: string, hander?: Function) { const loggerInfo =Object.seal({logId:logId, input:’’,output:’’, custom: ‘’}); const channelName = ‘__logger’; const msgChannel = postal.channel(channelName); msgChannel.subscribe(logId, logData => { // 根据业务逻辑来处理日志 console.log(logData); }); return function (target: any, key: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> { const oldValue = descriptor.value descriptor.value = function () { const args: Array<any> = []; for (let index in arguments) { args.push(arguments[index]); } loggerInfo.input = ${key}(${args.join(',')}); // 执行原方法 const value = oldValue.apply(this, arguments); loggerInfo.output = value; hander && (loggerInfo.custom = hander(loggerInfo.input, loggerInfo.output) || ‘’); // 被调用时,会自动发出一个事件 msgChannel.publish(logId, loggerInfo); } return descriptor }}使用// 直接使用非常简洁@Logger(’event_get_detial1’)getDetial(id?: string, category?: string) { return “详细内容”;}// 或者使用自定义,让日志和业务逻辑分离@Logger(’event_get_detial2’, (input, output) => { return ‘我是自定义内容’;})getDetial2(id?: string, category?: string) { return “详细内容”;}…<button @click=“getDetial2(‘1000’, ‘a’)">获取详情</button>效果: {logId: “event_get_detial2”, input: “getDetial2(1000,a)”, output: “详细内容”, custom: “我是自定义内容”}, 每次点击按钮都会触发一次。TODO: 这里还需要对输入参数和输出参数中的引用数据类型做处理。同时还需要掌握:装饰器工厂、装饰器组合、装饰器求值、参数装饰器、元数据哪些功能适合用Decorator实现官网和社区提供的这些Decorator, 可以作为自己框架的底层设计。日志功能全局都得用,调用方法基本一致,是最适合使用装饰器来实现,并且每个项目的日志记录各有差异,最适合自定义这部分。Decorator实现小Tips考虑下各类Decorator叠加和共存的问题,可以参考官网关于装饰器组合描述Decorator 的目标是在原有功能基础上,添加功能,切忌覆盖原有功能类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。类是不会提升的,所以就没有这方面的问题。注意迁移速度、避免一口吃成胖子的做法不要另起炉灶对主流库创建Decorator库,主流库维护成本很高还是得有官方来维护,为保证质量不使用个人编写的Decorator库。自己在创建Decorator库时也要有这个意识,仅做一些有必要自定义的。Decorator 不是管道模式,decorator之间不存在交互,所以必须注意保持decorator独立性、透明性Decorator 更适用于非业务功能需求确定 decorator 的用途后,切记执行判断参数类型decorator 针对每个装饰目标,仅执行一次See alsots官网装饰器说明【深入阅读】 https://www.tslang.cn/docs/ha...Decorator作用在不同的声明类型target、key说明 https://segmentfault.com/a/11...JS 装饰器(Decorator)场景实战 https://zhuanlan.zhihu.com/p/...Decorators in ES7 https://zhuanlan.zhihu.com/p/...vue组件 风格指南 https://cn.vuejs.org/v2/style…使用Typescript封装一款装饰器风格的Web框架 https://zhuanlan.zhihu.com/p/...Axios 装饰器实现 1.https://github.com/glangzh/re… 2.https://www.npmjs.com/package…声明计算属性用的。 https://my.oschina.net/lpcysz...autobind https://blog.csdn.net/liubigg...vue-class-component源码阅读 https://www.jianshu.com/p/cfe…函数和方法的区别 https://www.imooc.com/article…很多人整理过使用方法 https://www.cnblogs.com/navys…举个例子 join(separator?: string): string;vue源码解析系列-compute实现机制 https://segmentfault.com/a/11… ...

February 20, 2019 · 1 min · jiezi

ES6 系列之我们来聊聊装饰器

Decorator装饰器主要用于:装饰类装饰方法或属性装饰类@annotationclass MyClass { }function annotation(target) { target.annotated = true;}装饰方法或属性class MyClass { @readonly method() { }}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}Babel安装编译我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。不过我们也可以选择本地编译:npm initnpm install –save-dev @babel/core @babel/clinpm install –save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties新建 .babelrc 文件{ “plugins”: [ ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", {“loose”: true}] ]}再编译指定的文件babel decorator.js –out-file decorator-compiled.js装饰类的编译编译前:@annotationclass MyClass { }function annotation(target) { target.annotated = true;}编译后:var _class;let MyClass = annotation(_class = class MyClass {}) || _class;function annotation(target) { target.annotated = true;}我们可以看到对于类的装饰,其原理就是:@decoratorclass A {}// 等同于class A {}A = decorator(A) || A;装饰方法的编译编译前:class MyClass { @unenumerable @readonly method() { }}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}function unenumerable(target, name, descriptor) { descriptor.enumerable = false; return descriptor;}编译后:var _class;function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) { /** * 第一部分 * 拷贝属性 / var desc = {}; Object“ke” + “ys”.forEach(function(key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if (“value” in desc || desc.initializer) { desc.writable = true; } /* * 第二部分 * 应用多个 decorators / desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc); /* * 第三部分 * 设置要 decorators 的属性 / if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object[“define” + “Property”](target, property, desc); desc = null; } return desc;}let MyClass = ((_class = class MyClass { method() {}}),_applyDecoratedDescriptor( _class.prototype, “method”, [readonly], Object.getOwnPropertyDescriptor(_class.prototype, “method”), _class.prototype),_class);function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}装饰方法的编译源码解析我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。Object.getOwnPropertyDescriptor()在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:Object.getOwnPropertyDescriptor() 方法返回指定对象上的一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)顺便注意这是一个 ES5 的方法。举个例子:const foo = { value: 1 };const bar = Object.getOwnPropertyDescriptor(foo, “value”);// bar {// value: 1,// writable: true// enumerable: true,// configurable: true,// }const foo = { get value() { return 1; } };const bar = Object.getOwnPropertyDescriptor(foo, “value”);// bar {// get: /the getter function/,// set: undefined// enumerable: true,// configurable: true,// }第一部分源码解析在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:// 拷贝一份 descriptorvar desc = {};Object“ke” + “ys”.forEach(function(key) { desc[key] = descriptor[key];});desc.enumerable = !!desc.enumerable;desc.configurable = !!desc.configurable;// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setterif (“value” in desc || desc.initializer) { desc.writable = true;}那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:class MyClass { @readonly born = Date.now();}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}var foo = new MyClass();console.log(foo.born);Babel 就会编译为:// …(_descriptor = _applyDecoratedDescriptor(_class.prototype, “born”, [readonly], { configurable: true, enumerable: true, writable: true, initializer: function() { return Date.now(); }}))// …此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。第二部分源码解析接下是应用多个 decorators:/* * 第二部分 * @type {[type]} /desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc);对于一个方法应用了多个 decorator,比如:class MyClass { @unenumerable @readonly method() { }}Babel 会编译为:_applyDecoratedDescriptor( _class.prototype, “method”, [unenumerable, readonly], Object.getOwnPropertyDescriptor(_class.prototype, “method”), _class.prototype)在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。第三部分源码解析/* * 第三部分 * 设置要 decorators 的属性 /if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined;}if (desc.initializer === void 0) { Object[“define” + “Property”](target, property, desc); desc = null;}return desc;如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:desc.initializer.call(context)而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能class MyClass { @readonly value = this.getNum() + 1; getNum() { return 1; }}最后无论是装饰方法还是属性,都会执行:Object[“define” + “Property”](target, property, desc);由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。应用1.log为一个方法添加 log 函数,检查输入的参数:class Math { @log add(a, b) { return a + b; }}function log(target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function(…args) { console.log(Calling ${name} with, args); return oldValue.apply(this, args); }; return descriptor;}const math = new Math();// Calling add with [2, 4]math.add(2, 4);再完善点:let log = (type) => { return (target, name, descriptor) => { const method = descriptor.value; descriptor.value = (…args) => { console.info((${type}) 正在执行: ${name}(${args}) = ?); let ret; try { ret = method.apply(target, args); console.info((${type}) 成功 : ${name}(${args}) =&gt; ${ret}); } catch (error) { console.error((${type}) 失败: ${name}(${args}) =&gt; ${error}); } return ret; } }};2.autobindclass Person { @autobind getPerson() { return this; }}let person = new Person();let { getPerson } = person;getPerson() === person;// true我们很容易想到的一个场景是 React 绑定事件的时候:class Toggle extends React.Component { @autobind handleClick() { console.log(this) } render() { return ( <button onClick={this.handleClick}> button </button> ); }}我们来写这样一个 autobind 函数:const { defineProperty, getPrototypeOf} = Object;function bind(fn, context) { if (fn.bind) { return fn.bind(context); } else { return function autobind() { return fn.apply(context, arguments); }; }}function createDefaultSetter(key) { return function set(newValue) { Object.defineProperty(this, key, { configurable: true, writable: true, enumerable: true, value: newValue }); return newValue; };}function autobind(target, key, { value: fn, configurable, enumerable }) { if (typeof fn !== ‘function’) { throw new SyntaxError(@autobind can only be used on functions, not: ${fn}); } const { constructor } = target; return { configurable, enumerable, get() { /* * 使用这种方式相当于替换了这个函数,所以当比如 * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回 * 所以这里做了 this 的判断 */ if (this === target) { return fn; } const boundFn = bind(fn, this); defineProperty(this, key, { configurable: true, writable: true, enumerable: false, value: boundFn }); return boundFn; }, set: createDefaultSetter(key) };}3.debounce有的时候,我们需要对执行的方法进行防抖处理: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() } }; }}4.time用于统计方法执行的时间: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); } } } }}5.mixin用于将对象的方法混入 Class 中:const SingerMixin = { sing(sound) { alert(sound); }};const FlyMixin = { // All types of property descriptors are supported get speed() {}, fly() {}, land() {}};@mixin(SingerMixin, FlyMixin)class Bird { singMatingCall() { this.sing(’tweet tweet’); }}var bird = new Bird();bird.singMatingCall();// alerts “tweet tweet"mixin 的一个简单实现如下:function mixin(…mixins) { return target => { if (!mixins.length) { throw new SyntaxError(@mixin() class ${target.name} requires at least one mixin as an argument); } for (let i = 0, l = mixins.length; i < l; i++) { const descs = Object.getOwnPropertyDescriptors(mixins[i]); const keys = Object.getOwnPropertyNames(descs); for (let j = 0, k = keys.length; j < k; j++) { const key = keys[j]; if (!target.prototype.hasOwnProperty(key)) { Object.defineProperty(target.prototype, key, descs[key]); } } } };}6.redux实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);有了装饰器,就可以改写上面的代码。@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {};相对来说,后一种写法看上去更容易理解。7.注意以上我们都是用于修饰类方法,我们获取值的方式为:const method = descriptor.value;但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:const value = descriptor.initializer && descriptor.initializer();参考ECMAScript 6 入门core-decoratorsES7 Decorator 装饰者模式JS 装饰器(Decorator)场景实战ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 15, 2018 · 6 min · jiezi