资讯专栏INFORMATION COLUMN

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

XanaHopper / 1982人阅读

摘要:描述符必须是这两种形式之一不能同时是两者。可以是任何有效的值数值,对象,函数等。当且仅当该属性的为时,才能被赋值运算符改变。特点就是不影响之前对象的特性,而新增额外的职责功能。

前言

</>复制代码

  1. 原文链接:[Nealyang/personalBlog]()

ES6 已经不必在过多介绍,在 ES6 之前,装饰器可能并没有那么重要,因为你只需要加一层 wrapper 就好了,但是现在,由于语法糖 class 的出现,当我们想要去在多个类之间共享或者扩展一些方法的时候,代码会变得错综复杂,难以维护,而这,也正式我们 Decorator 的用武之地。

Object.defineProperty

关于 Object.defineProperty 简单的说,就是该方法可以精准的添加和修改对象的属性

语法

Object.defineProperty(obj,prop,descriptor)

ojb:要在其上定义属性的对象

prop:要定义或修改的属性的名称

descriptor:将被定义或修改的属性描述符

该方法返回被传递给函数的对象

</>复制代码

  1. 在ES6中,由于 Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而Object.defineProperty 是定义key为Symbol的属性的方法之一。

通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来(for...in 或 Object.keys 方法), 这些属性的值可以被改变,也可以被删除。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的

属相描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

数据描述符和存取描述符均具有以下可选键值:

</>复制代码

  1. configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false

</>复制代码

  1. enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

</>复制代码

  1. value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

</>复制代码

  1. writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false

存取描述符同时具有以下可选键值:

</>复制代码

  1. get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。

</>复制代码

  1. set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。

如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常

更多使用实例和介绍,参看:MDN

装饰者模式

在看Decorator之前,我们先看下装饰者模式的使用,我们都知道,装饰者模式能够在不改变对象自身基础上,在程序运行期间给对象添加指责。特点就是不影响之前对象的特性,而新增额外的职责功能。

like...this:

这段比较简单,直接看代码吧:

</>复制代码

  1. let Monkey = function () {}
  2. Monkey.prototype.say = function () {
  3. console.log("目前我只是个野猴子");
  4. }
  5. let TensionMonkey = function (monkey) {
  6. this.monkey = monkey;
  7. }
  8. TensionMonkey.prototype.say = function () {
  9. this.monkey.say();
  10. console.log("带上紧箍咒,我就要忘记世间烦恼!");
  11. }
  12. let monkey = new TensionMonkey(new Monkey());
  13. monkey.say();

执行结果:

Decorator

Decorator其实就是一个语法糖,背后其实就是利用es5的Object.defineProperty(target,name,descriptor),了解Object.defineProperty请移步这个链接:MDN文档

其背后原理大致如下:

</>复制代码

  1. class Monkey{
  2. say(){
  3. console.log("目前,我只是个野猴子");
  4. }
  5. }

执行上面的代码,大致代码如下:

</>复制代码

  1. Object.defineProperty(Monkey.prototype,"say",{
  2. value:function(){console.log("目前,我只是个野猴子")},
  3. enumerable:false,
  4. configurable:true,
  5. writable:true
  6. })

如果我们利用装饰器来修饰他

</>复制代码

  1. class Monkey{
  2. @readonly
  3. say(){console.log("现在我是只读的了")}
  4. }

在这种装饰器的属性,会在Object.defineProperty为Monkey.prototype注册say属性之前,执行以下代码:

</>复制代码

  1. let descriptor = {
  2. value:specifiedFunction,
  3. enumerable:false,
  4. configurable:true,
  5. writeable:true
  6. };
  7. descriptor = readonly(Monkey.prototype,"say",descriptor)||descriptor;
  8. Object.defineProperty(Monkey.prototype,"say",descriptor);

从上面的伪代码我们可以看出,Decorator只是在Object.defineProperty为Monkey.prototype注册属性之前,执行了一个装饰函数,其属于一个类对Object.defineProperty的拦截。所以它和Object.defineProperty具有一致的形参:

obj:作用的目标对象

prop:作用的属性名

descriptor:针对该属性的描述符

下面看下简单的使用

在class中的使用

创建一个新的class继承自原有的class,并添加属性

</>复制代码

  1. @name
  2. class Person{
  3. sayHello(){
  4. console.log(`hello ,my name is ${this.name}`)
  5. }
  6. }
  7. function name(constructor) {
  8. return class extends constructor{
  9. name="Nealyang"
  10. }
  11. }
  12. new Person().sayHello()
  13. //hello ,my name is Nealyang

针对当前class修改(类似mixin)

</>复制代码

  1. @name
  2. @seal
  3. class Person {
  4. sayHello() {
  5. console.log(`hello ,my name is ${this.name}`)
  6. }
  7. }
  8. function name(constructor) {
  9. Object.defineProperty(constructor.prototype,"name",{
  10. value:"一凨"
  11. })
  12. }
  13. new Person().sayHello()
  14. //若修改一个属性
  15. function seal(constructor) {
  16. let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, "sayHello")
  17. Object.defineProperty(constructor.prototype, "sayHello", {
  18. ...descriptor,
  19. writable: false
  20. })
  21. }
  22. new Person().sayHello = 1;// Cannot assign to read only property "sayHello" of object "#"

上面说到mixin,那么我就来模拟一个mixin吧

</>复制代码

  1. class A {
  2. run() {
  3. console.log("我会跑步!")
  4. }
  5. }
  6. class B {
  7. jump() {
  8. console.log("我会跳!")
  9. }
  10. }
  11. @mixin(A, B)
  12. class C {}
  13. function mixin(...args) {
  14. return function (constructor) {
  15. for (const arg of args) {
  16. for (let key of Object.getOwnPropertyNames(arg.prototype)) {
  17. if (key === "constructor") continue;
  18. Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key));
  19. }
  20. }
  21. }
  22. }
  23. let c = new C();
  24. c.jump();
  25. c.run();
  26. // 我会跳!
  27. // 我会跑步!

截止目前我們貌似写了非常多的代码了,对。。。这篇,为了彻底搞投Decorator,这。。。只是开始。。。

在class成员中的使用

这类的装饰器的写法应该就是我们最为熟知了,会接受三个参数:

如果装饰器挂载在静态成员上,则会返回构造函数,如果挂载在实例成员上,则返回类的原型

装饰器挂载的成员名称

Object.getOwnPropertyDescriptor的返回值

首先,我们明确下静态成员和实例成员的区别

</>复制代码

  1. class Model{
  2. //实例成员
  3. method1(){}
  4. method2 = ()=>{}
  5. // 靜態成員
  6. static method3(){}
  7. static method4 = ()=>{}
  8. }

method1 和method2 是实例成员,但是method1存在于prototype上,method2只有实例化对象以后才有。

method3和method4是静态成员,两者的区别在于是否可枚举描述符的设置,我们通过babel转码可以看到:

上述代码比较乱,简单的可以理解为:

</>复制代码

  1. function Model () {
  2. // 成员仅在实例化时赋值
  3. this.method2 = function () {}
  4. }
  5. // 成员被定义在原型链上
  6. Object.defineProperty(Model.prototype, "method1", {
  7. value: function () {},
  8. writable: true,
  9. enumerable: false, // 设置不可被枚举
  10. configurable: true
  11. })
  12. // 成员被定义在构造函数上,且是默认的可被枚举
  13. Model.method4 = function () {}
  14. // 成员被定义在构造函数上
  15. Object.defineProperty(Model, "method3", {
  16. value: function () {},
  17. writable: true,
  18. enumerable: false, // 设置不可被枚举
  19. configurable: true
  20. })

可以看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,所以这就是为什么在针对Property Decorator不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor,目前没有找到合理的解释,但是如果明确的要使用,是可以手动获取的。

就像上述的示例,我们针对四个成员都添加了装饰器以后,method1和method2第一个参数就是Model.prototype,而method3和method4的第一个参数就是Model。

</>复制代码

  1. class Model {
  2. // 实例成员
  3. @instance
  4. method1 () {}
  5. @instance
  6. method2 = () => {}
  7. // 静态成员
  8. @static
  9. static method3 () {}
  10. @static
  11. static method4 = () => {}
  12. }
  13. function instance(target) {
  14. console.log(target.constructor === Model)
  15. }
  16. function static(target) {
  17. console.log(target === Model)
  18. }
函数、访问器、属性 三者装饰器的使用

函数装饰器的返回值会默认作为属性的value描述符的存在,如果返回为undefined则忽略

</>复制代码

  1. class Model {
  2. @log1
  3. getData1() {}
  4. @log2
  5. getData2() {}
  6. }
  7. // 方案一,返回新的value描述符
  8. function log1(tag, name, descriptor) {
  9. return {
  10. ...descriptor,
  11. value(...args) {
  12. let start = new Date().valueOf()
  13. try {
  14. return descriptor.value.apply(this, args)
  15. } finally {
  16. let end = new Date().valueOf()
  17. console.log(`start: ${start} end: ${end} consume: ${end - start}`)
  18. }
  19. }
  20. }
  21. }
  22. // 方案二、修改现有描述符
  23. function log2(tag, name, descriptor) {
  24. let func = descriptor.value // 先获取之前的函数
  25. // 修改对应的value
  26. descriptor.value = function (...args) {
  27. let start = new Date().valueOf()
  28. try {
  29. return func.apply(this, args)
  30. } finally {
  31. let end = new Date().valueOf()
  32. console.log(`start: ${start} end: ${end} consume: ${end - start}`)
  33. }
  34. }
  35. }

访问器的Decorator就是get set前缀函数了,用于控制属性的赋值、取值操作,在使用上和函数装饰器没有任何区别

</>复制代码

  1. class Modal {
  2. _name = "Niko"
  3. @prefix
  4. get name() { return this._name }
  5. }
  6. function prefix(target, name, descriptor) {
  7. return {
  8. ...descriptor,
  9. get () {
  10. return `wrap_${this._name}`
  11. }
  12. }
  13. }
  14. console.log(new Modal().name) // wrap_Niko

对于属性装饰器是没有descriptor返回的,并且装饰器函数的返回值也会被忽略,如果我们需要修改某一个静态属性,则需要自己获取descriptor

</>复制代码

  1. class Modal {
  2. @prefix
  3. static name1 = "Niko"
  4. }
  5. function prefix(target, name) {
  6. let descriptor = Object.getOwnPropertyDescriptor(target, name)
  7. Object.defineProperty(target, name, {
  8. ...descriptor,
  9. value: `wrap_${descriptor.value}`
  10. })
  11. }
  12. console.log(Modal.name1) // wrap_Niko

对于一个实例的属性,则没有直接修改的方案,不过我们可以结合着一些其他装饰器来曲线救国。

比如,我们有一个类,会传入姓名和年龄作为初始化的参数,然后我们要针对这两个参数设置对应的格式校验

</>复制代码

  1. const validateConf = {} // 存储校验信息
  2. @validator
  3. class Person {
  4. @validate("string")
  5. name
  6. @validate("number")
  7. age
  8. constructor(name, age) {
  9. this.name = name
  10. this.age = age
  11. }
  12. }
  13. function validator(constructor) {
  14. return class extends constructor {
  15. constructor(...args) {
  16. super(...args)
  17. // 遍历所有的校验信息进行验证
  18. for (let [key, type] of Object.entries(validateConf)) {
  19. if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
  20. }
  21. }
  22. }
  23. }
  24. function validate(type) {
  25. return function (target, name, descriptor) {
  26. // 向全局对象中传入要校验的属性名及类型
  27. validateConf[name] = type
  28. }
  29. }
  30. new Person("Niko", "18") // throw new error: [age must be number]
函数参数装饰器

</>复制代码

  1. const parseConf = {}
  2. class Modal {
  3. @parseFunc
  4. addOne(@parse("number") num) {
  5. return num + 1
  6. }
  7. }
  8. // 在函数调用前执行格式化操作
  9. function parseFunc (target, name, descriptor) {
  10. return {
  11. ...descriptor,
  12. value (...arg) {
  13. // 获取格式化配置
  14. for (let [index, type] of parseConf) {
  15. switch (type) {
  16. case "number": arg[index] = Number(arg[index]) break
  17. case "string": arg[index] = String(arg[index]) break
  18. case "boolean": arg[index] = String(arg[index]) === "true" break
  19. }
  20. return descriptor.value.apply(this, arg)
  21. }
  22. }
  23. }
  24. }
  25. // 向全局对象中添加对应的格式化信息
  26. function parse(type) {
  27. return function (target, name, index) {
  28. parseConf[index] = type
  29. }
  30. }
  31. console.log(new Modal().addOne("10")) // 11
Decorator 用例

log

为一个方法添加 log 函数,检查输入的参数

</>复制代码

  1. let log = type => {
  2. return (target,name,decorator) => {
  3. const method = decorator.value;
  4. console.log(method);
  5. decorator.value = (...args) => {
  6. console.info(`${type} 正在进行:${name}(${args}) = ?`);
  7. let result;
  8. try{
  9. result = method.apply(target,args);
  10. console.info(`(${type}) 成功 : ${name}(${args}) => ${result}`);
  11. }catch(err){
  12. console.error(`(${type}) 失败: ${name}(${args}) => ${err}`);
  13. }
  14. return result;
  15. }
  16. }
  17. }
  18. class Math {
  19. @log("add")
  20. add(a, b) {
  21. return a + b;
  22. }
  23. }
  24. const math = new Math();
  25. // (add) 成功 : add(2,4) => 6
  26. math.add(2, 4);

time

用于统计方法执行的时间:

</>复制代码

  1. function time(prefix) {
  2. let count = 0;
  3. return function handleDescriptor(target, key, descriptor) {
  4. const fn = descriptor.value;
  5. if (prefix == null) {
  6. prefix = `${target.constructor.name}.${key}`;
  7. }
  8. if (typeof fn !== "function") {
  9. throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
  10. }
  11. return {
  12. ...descriptor,
  13. value() {
  14. const label = `${prefix}-${count}`;
  15. count++;
  16. console.time(label);
  17. try {
  18. return fn.apply(this, arguments);
  19. } finally {
  20. console.timeEnd(label);
  21. }
  22. }
  23. }
  24. }
  25. }
debounce

对执行的方法进行防抖处理

</>复制代码

  1. class Toggle extends React.Component {
  2. @debounce(500, true)
  3. handleClick() {
  4. console.log("toggle")
  5. }
  6. render() {
  7. return (
  8. button
  9. );
  10. }
  11. }
  12. function _debounce(func, wait, immediate) {
  13. var timeout;
  14. return function () {
  15. var context = this;
  16. var args = arguments;
  17. if (timeout) clearTimeout(timeout);
  18. if (immediate) {
  19. var callNow = !timeout;
  20. timeout = setTimeout(function(){
  21. timeout = null;
  22. }, wait)
  23. if (callNow) func.apply(context, args)
  24. }
  25. else {
  26. timeout = setTimeout(function(){
  27. func.apply(context, args)
  28. }, wait);
  29. }
  30. }
  31. }
  32. function debounce(wait, immediate) {
  33. return function handleDescriptor(target, key, descriptor) {
  34. const callback = descriptor.value;
  35. if (typeof callback !== "function") {
  36. throw new SyntaxError("Only functions can be debounced");
  37. }
  38. var fn = _debounce(callback, wait, immediate)
  39. return {
  40. ...descriptor,
  41. value() {
  42. fn()
  43. }
  44. };
  45. }
  46. }

</>复制代码

  1. 更多关于 core-decorators 的例子后面再 Nealyang/PersonalBlog中补充,再加注释说明。
参考

Javascript装饰器的妙用

ES7 Decorator 装饰者模式

ES7之Decorators实现AOP示例

细说ES7 JavaScript Decorators

Using ES.later Decorators as Mixins

core-decorators

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103776.html

相关文章

  • windows下安装docker

    摘要:如下图,我打开虚拟化之后,还是不能直观的看到我是否打开了虚拟化。注群里有朋友说上的一个玩意儿报错,可以直接根据那个地址下载,我看其它帖子都说放到这个下头去,如果碰到那个错误了可以试试 作为伟大的21世纪接班人,怎么呢不会docker技术呢?好吧,由于种种原因,我的几块钱云服务器docker不能动它的配置,于是就想在win7上装一个练练手~ 1、下载windows下的docker工具do...

    Yuqi 评论0 收藏0
  • windows下安装docker

    摘要:如下图,我打开虚拟化之后,还是不能直观的看到我是否打开了虚拟化。注群里有朋友说上的一个玩意儿报错,可以直接根据那个地址下载,我看其它帖子都说放到这个下头去,如果碰到那个错误了可以试试 作为伟大的21世纪接班人,怎么呢不会docker技术呢?好吧,由于种种原因,我的几块钱云服务器docker不能动它的配置,于是就想在win7上装一个练练手~ 1、下载windows下的docker工具do...

    李文鹏 评论0 收藏0
  • FEDAY2016之旅

    摘要:前戏补上参会的完整记录,这个问题从一开始我就是准备自问自答的,希望可以通过这种形式把大会的干货分享给更多人。 showImg(http://7xqy7v.com1.z0.glb.clouddn.com/colorful/blog/feday2.png); 前戏 2016/3/21 补上参会的完整记录,这个问题从一开始我就是准备自问自答的,希望可以通过这种形式把大会的干货分享给更多人。 ...

    red_bricks 评论0 收藏0

发表评论

0条评论

XanaHopper

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<