Decorator

装饰器主要用于:

  1. 装饰类
  2. 装饰方法或属性

装饰类

@annotation
class 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 init

npm install --save-dev @babel/core @babel/cli

npm 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

装饰类的编译

编译前:

@annotation
class MyClass { } function annotation(target) {
target.annotated = true;
}

编译后:

var _class;

let MyClass = annotation(_class = class MyClass {}) || _class;

function annotation(target) {
target.annotated = true;
}

我们可以看到对于类的装饰,其原理就是:

@decorator
class 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"](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;
} /**
* 第二部分
* 应用多个 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() 返回的属性描述符对象做了一份拷贝:

// 拷贝一份 descriptor
var desc = {};
Object["ke" + "ys"](descriptor).forEach(function(key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable; // 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter
if ("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}) => ${ret}`);
} catch (error) {
console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
}
return ret;
}
}
};

2.autobind

class 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();

原文链接
本文为云栖社区原创内容,未经允许不得转载。

ES6 系列之我们来聊聊装饰器的更多相关文章

  1. Flask系列06--(中间件)Flask的特殊装饰器 before_request,after_request, errorhandler

    一.使用 Flask中的特殊装饰器(中间件)方法常用的有三个 @app.before_request # 在请求进入视图函数之前 @app.after_request # 在请求结束视图函数之后 响应 ...

  2. 基于TypeScript装饰器定义Express RESTful 服务

    前言 本文主要讲解如何使用TypeScript装饰器定义Express路由.文中出现的代码经过简化不能直接运行,完整代码的请戳:https://github.com/WinfredWang/expre ...

  3. TypeScript装饰器(decorators)

    装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,可以修改类的行为. 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被 ...

  4. Fastify 系列教程二 (中间件、钩子函数和装饰器)

    Fastify 系列教程: Fastify 系列教程一 (路由和日志) Fastify 系列教程二 (中间件.钩子函数和装饰器) 中间件 Fastify 提供了与 Express 和 Restify ...

  5. Java设计模式系列-装饰器模式

    原创文章,转载请标注出处:<Java设计模式系列-装饰器模式> 一.概述 装饰器模式作用是针对目标方法进行增强,提供新的功能或者额外的功能. 不同于适配器模式和桥接模式,装饰器模式涉及的是 ...

  6. Fastify 系列教程二 (中间件、钩子函数和装饰器)

    Fastify 系列教程: Fastify 系列教程一 (路由和日志) Fastify 系列教程二 (中间件.钩子函数和装饰器) Fastify 系列教程三 (验证.序列化和生命周期) Fastify ...

  7. 从ES6重新认识JavaScript设计模式: 装饰器模式

    1 什么是装饰器模式 向一个现有的对象添加新的功能,同时又不改变其结构的设计模式被称为装饰器模式(Decorator Pattern),它是作为现有的类的一个包装(Wrapper). 可以将装饰器理解 ...

  8. 一篇夯实一个知识点系列--python装饰器

    写在前面 本系列目的:希望可以通过一篇文章,不望鞭辟入里,但求在工程应用中得心应手. 装饰器模式是鼎鼎大名的23种设计模式之一.装饰器模式可以在不改变原有代码结构的情况下,扩展代码功能. Python ...

  9. python cookbook 学习系列(一) python中的装饰器

    简介 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象.它经常用于有切面需求的场景,比如:插入日志.性能测试.事务处理.缓 ...

随机推荐

  1. 周报数据采集之生存图片(execl方法)

    https://blog.csdn.net/Luzaofa/article/details/81675364 Python之Excel chart另存为图片大家好,好久没有更新博客了,这一段时间有点忙 ...

  2. TCP的三次握手 与 四次挥手

    TCP的连接建立是一个三次握手过程,目的是为了通信双方确认开始序号,以便后续通信的有序进行 主要步骤: 服务器一定处于Listen状态,否则客户端发过来的连接会被拒绝.注:服务器和客户端的角色是相对的 ...

  3. LAPM 相关实验01

    目录 lab1 静态.动态资源的区别lab2 部署phpMyadminlab3 部署wordpresslab4 编译安装php-Xcache加速器lab5 fcgi实现lamp lab1 静态.动态资 ...

  4. [f]聊天的时间格式化

    代码如下: 参数: t: 时间戳, type:返回格式(1:IM界面,其他:会话列表) function formartTime(t, type) { var oldtime = new Date(t ...

  5. 【转载】java定义二维数组问题。分清数组与集合的区别

    出处: 度娘知道 答案由用户{ heitianba }提供. Q: int a[][] = new int[3][2];  a[0] = {1,6};  报错:第二句是非法表达式.为什么? A: in ...

  6. python猜数字游戏console版本

    加入python学习小组后的第一次作业,python GUI写猜数字游戏.由于加班比较多,第一步先实现console版本,下一步再实现GUI版本. 虽然猜数字游戏是个小游戏,但是涉及到的基础知识点还是 ...

  7. NSInvalidArgumentException这个到底是什么意思,我到网上查了下,每个的错误都不同。

    我几乎把所有的东西都注释了,但还是崩了 #import "ViewController.h" //#import "WeiBo.h" @interface Vi ...

  8. 14.不同条目的listview

    分类界面 整个项目的逻辑就是这样的 CategoryInfo  public class CategoryInfo { private String title; private String url ...

  9. 聊一聊PHP的global

    众所周知,在PHP的函数中,如果想使用全局变量,一种是使用超全局变量$GLOBALS,另一种是在函数中使用global关键字声明,使用超全局变量$GLOBALS的方式大家都知道了,今天来好好聊一聊使用 ...

  10. MySQL 报错ERROR 1054 (42S22): Unknown column 'plugin' in 'mysql.user'

    MySQL  我们在创建用户的时候,可能会遇到以下报错: ERROR 1054 (42S22): Unknown column 'plugin' in 'mysql.user' 说明mysq.user ...