JS装饰器函数用法总结


Posted in Javascript onApril 21, 2018

在 ES6 中增加了对类对象的相关定义和操作(比如 class 和 extends ),这就使得我们在多个不同类之间共享或者扩展一些方法或者行为的时候,变得并不是那么优雅。这个时候,我们就需要一种更优雅的方法来帮助我们完成这些事情。

什么是装饰器

Python 的装饰器

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持 OOP 的 decorator 外,直接从语法层次支持 decorator。

如果你熟悉 python 的话,对它一定不会陌生。那么我们先来看一下 python 里的装饰器是什么样子的吧:

def decorator(f):
  print "my decorator"
  return f
@decorator
def myfunc():
  print "my function"
myfunc()
# my decorator
# my function

这里的 @decorator 就是我们说的装饰器。在上面的代码中,我们利用装饰器给我们的目标方法执行前打印出了一行文本,并且并没有对原方法做任何的修改。代码基本等同于:

def decorator(f):
  def wrapper():
    print "my decorator"
    return f()
  return wrapper
def myfunc():
  print "my function"
myfunc = decorator(myfuc)

通过代码我们也不难看出,装饰器 decorator 接收一个参数,也就是我们被装饰的目标方法,处理完扩展的内容以后再返回一个方法,供以后调用,同时也失去了对原方法对象的访问。当我们对某个应用了装饰以后,其实就改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现我们对原函数的扩展、修改等操作。

ES7 的装饰器

ES7 中的 decorator 同样借鉴了这个语法糖,不过依赖于 ES5 的 Object.defineProperty 方法 。

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

该方法允许精确添加或修改对象的属性。通过赋值来添加的普通属性会创建在属性枚举期间显示的属性(for...in 或 Object.keys 方法), 这些值可以被改变,也可以被删除。这种方法允许这些额外的细节从默认值改变。默认情况下,使用 Object.defineProperty() 添加的属性值是不可变的。

语法

Object.defineProperty(obj, prop, descriptor)
  1. obj:要在其上定义属性的对象。
  2. prop:要定义或修改的属性的名称。
  3. descriptor:将被定义或修改的属性描述符。
  4. 返回值:被传递给函数的对象。

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

属性描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。

数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。

  • 存取描述符是由 getter-setter 函数对描述的属性。
  • 描述符必须是这两种形式之一;不能同时是两者。

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

configurable

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

enumerable

enumerable定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。

当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:

value

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

writable

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

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

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

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

类的装饰

@testable
class MyTestableClass {
 // ...
}

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

MyTestableClass.isTestable // true

上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable 函数的参数 target 是 MyTestableClass 类本身。

基本上,装饰器的行为就是下面这样。

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类。

如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。

function testable(isTestable) {
 return function(target) {
  target.isTestable = isTestable;
 }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上面代码中,装饰器 testable 可以接受参数,这就等于可以修改装饰器的行为。

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的 prototype 对象操作。

下面是另外一个例子。

// mixins.js
export function mixins(...list) {
 return function (target) {
  Object.assign(target.prototype, ...list)
 }
}

// main.js
import { mixins } from './mixins'

const Foo = {
 foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'

上面代码通过装饰器 mixins,把Foo对象的方法添加到了 MyClass 的实例上面。

方法的装饰

装饰器不仅可以装饰类,还可以装饰类的属性。

class Person {
 @readonly
 name() { return `${this.first} ${this.last}` }
}

上面代码中,装饰器 readonly 用来装饰“类”的name方法。

装饰器函数 readonly 一共可以接受三个参数。

function readonly(target, name, descriptor){
 // descriptor对象原来的值如下
 // {
 //  value: specifiedFunction,
 //  enumerable: false,
 //  configurable: true,
 //  writable: true
 // };
 descriptor.writable = false;
 return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
  • 装饰器第一个参数是 类的原型对象,上例是 Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);
  • 第二个参数是 所要装饰的属性名
  • 第三个参数是 该属性的描述对象

另外,上面代码说明,装饰器(readonly)会修改属性的 描述对象(descriptor),然后被修改的描述对象再用来定义属性。

函数方法的装饰

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行。

function doSomething(name) {
 console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
 return function() {
  console.log('Starting');
  const result = wrapped.apply(this, arguments);
  console.log('Finished');
  return result;
 }
}

const wrapped = loggingDecorator(doSomething);

core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。

@autobind

autobind 装饰器使得方法中的this对象,绑定原始对象。

@readonly

readonly 装饰器使得属性或方法不可写。

@override

override 装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

import { override } from 'core-decorators';

class Parent {
 speak(first, second) {}
}

class Child extends Parent {
 @override
 speak() {}
 // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
 @override
 speaks() {}
 // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
 //
 //  Did you mean "speak"?
}

@deprecate (别名@deprecated)

deprecate 或 deprecated 装饰器在控制台显示一条警告,表示该方法将废除。

import { deprecate } from 'core-decorators';

class Person {
 @deprecate
 facepalm() {}

 @deprecate('We stopped facepalming')
 facepalmHard() {}

 @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
 facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//   See http://knowyourmeme.com/memes/facepalm for more details.
//

@suppressWarnings

suppressWarnings 装饰器抑制 deprecated 装饰器导致的 console.warn() 调用。但是,异步代码发出的调用除外。

使用场景

装饰器有注释的作用

@testable
class Person {
 @readonly
 @nonenumerable
 name() { return `${this.first} ${this.last}` }
}

有了装饰器,就可以改写上面的代码。装饰

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

相对来说,后一种写法看上去更容易理解。

新功能提醒或权限

菜单点击时,进行事件拦截,若该菜单有新功能更新,则弹窗显示。

/**
 * @description 在点击时,如果有新功能提醒,则弹窗显示
 * @param code 新功能的code
 * @returns {function(*, *, *)}
 */
 const checkRecommandFunc = (code) => (target, property, descriptor) => {
  let desF = descriptor.value; 
  descriptor.value = function (...args) {
   let recommandFuncModalData = SYSTEM.recommandFuncCodeMap[code];

   if (recommandFuncModalData && recommandFuncModalData.id) {
    setTimeout(() => {
     this.props.dispatch({type: 'global/setRecommandFuncModalData', recommandFuncModalData});
    }, 1000);
   }
   desF.apply(this, args);
  };
  return descriptor;
 };

loading

在 React 项目中,我们可能需要在向后台请求数据时,页面出现 loading 动画。这个时候,你就可以使用装饰器,优雅地实现功能。

@autobind
@loadingWrap(true)
async handleSelect(params) {
 await this.props.dispatch({
  type: 'product_list/setQuerypParams',
  querypParams: params
 });
}

loadingWrap 函数如下:、

export function loadingWrap(needHide) {

 const defaultLoading = (
  <div className="toast-loading">
   <Loading className="loading-icon"/>
   <div>加载中...</div>
  </div>
 );

 return function (target, property, descriptor) {
  const raw = descriptor.value;
  
  descriptor.value = function (...args) {
   Toast.info(text || defaultLoading, 0, null, true);
   const res = raw.apply(this, args);
   
   if (needHide) {
    if (get('finally')(res)) {
     res.finally(() => {
      Toast.hide();
     });
    } else {
     Toast.hide();
    }
   }
  };
  return descriptor;
 };
}

问题:这里大家可以想想看,如果我们不希望每次请求数据时都出现 loading,而是要求只要后台请求时间大于 300ms 时,才显示loading,这里需要怎么改?

以上就是本次小编整理的关于JS装饰器的相关知识点,感谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Mootools 1.2教程 定时器和哈希简介
Sep 15 Javascript
JavaScript prototype对象的属性说明
Mar 13 Javascript
一个简单的jQuery插件制作 学习过程及实例
Apr 25 Javascript
javascript实现图片切换的幻灯片效果源代码
Dec 12 Javascript
js 鼠标移动显示图片的简单实例
Dec 25 Javascript
JavaScript中一个奇葩的IE浏览器判断方法
Apr 16 Javascript
Bootstrap实现各种进度条样式详解
Apr 13 Javascript
微信小程序tabBar用法实例详解
Dec 04 Javascript
解决vue 格式化银行卡(信用卡)每4位一个符号隔断的问题
Sep 14 Javascript
bootstrap下拉分页样式 带跳转页码
Dec 29 Javascript
可拖拽组件slider.js使用方法详解
Dec 04 Javascript
canvas绘制折线路径动画实现
May 12 Javascript
vue 之 .sync 修饰符示例详解
Apr 21 #Javascript
关于echarts在节点显示动态数据及添加提示文本所遇到的问题
Apr 20 #Javascript
vue中v-cloak解决刷新或者加载出现闪烁问题(显示变量)
Apr 20 #Javascript
jQuery中的$是什么意思及 $. 和 $().的区别
Apr 20 #jQuery
vue多页面开发和打包正确处理方法
Apr 20 #Javascript
用ES6的class模仿Vue写一个双向绑定的示例代码
Apr 20 #Javascript
Vue写一个简单的倒计时按钮功能
Apr 20 #Javascript
You might like
用PHP读取IMAP邮件
2006/10/09 PHP
网站当前的在线人数
2006/10/09 PHP
php实现mysql事务处理的方法
2014/12/25 PHP
PHP采用超长(超大)数字运算防止数字以科学计数法显示的方法
2016/04/01 PHP
js中的前绑定和后绑定详解
2013/08/01 Javascript
原生js制作简单的数字键盘
2015/04/24 Javascript
JS+CSS实现仿msn风格选项卡效果代码
2015/10/22 Javascript
JavaScript实现阿拉伯数字和中文数字互相转换
2016/06/12 Javascript
bootstrap学习笔记之初识bootstrap
2016/06/21 Javascript
前端框架Vue.js构建大型应用浅析
2016/09/12 Javascript
详解vue2.0 不同屏幕适配及px与rem转换问题
2018/02/23 Javascript
Vue隐藏显示、只读实例代码
2018/07/18 Javascript
JS使用栈判断给定字符串是否是回文算法示例
2019/03/04 Javascript
js实现点击按钮随机生成背景颜色
2020/09/05 Javascript
python通过floor函数舍弃小数位的方法
2015/03/17 Python
基于python中的TCP及UDP(详解)
2017/11/06 Python
解决Django一个表单对应多个按钮的问题
2019/07/18 Python
Python中IP地址处理IPy模块的方法
2019/08/16 Python
基于django传递数据到后端的例子
2019/08/16 Python
如何在mac环境中用python处理protobuf
2019/12/25 Python
Python中无限循环需要什么条件
2020/05/27 Python
keras CNN卷积核可视化,热度图教程
2020/06/22 Python
Pytorch框架实现mnist手写库识别(与tensorflow对比)
2020/07/20 Python
英国家喻户晓的高街品牌:River Island
2017/11/28 全球购物
以下为Windows NT 下的32 位C++程序,请计算sizeof 的值
2016/12/07 面试题
abstract是什么意思
2012/02/12 面试题
安全生产演讲稿
2014/05/09 职场文书
文员转正自我鉴定怎么写
2014/09/29 职场文书
2014年团支部年度工作总结
2014/12/24 职场文书
房地产公司财务总监岗位职责
2015/04/03 职场文书
小学教研工作总结2015
2015/05/13 职场文书
培训班开班主持词
2015/07/02 职场文书
2015年迎新晚会策划书
2015/07/16 职场文书
《童年的发现》教学反思
2016/02/18 职场文书
叶县这家生产军用电台的兵工厂,人称“四机部”,走出一上将
2022/02/18 无线电
实战Python爬虫爬取酷我音乐
2022/04/11 Python