前言

原文链接: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:

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

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

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具有一致的形参:

  • obj:作用的目标对象
  • prop:作用的属性名
  • descriptor:针对该属性的描述符

下面看下简单的使用

在class中的使用

  • 创建一个新的class继承自原有的class,并添加属性
@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
  • 针对当前class修改(类似mixin)
@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成员中的使用

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

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

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

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)
}

函数、访问器、属性 三者装饰器的使用

  • 函数装饰器的返回值会默认作为属性的value描述符的存在,如果返回为undefined则忽略
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}`)
}
}
}
} // 方案二、修改现有描述符
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}`)
}
}
}
  • 访问器的Decorator就是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
  • 对于属性装饰器是没有descriptor返回的,并且装饰器函数的返回值也会被忽略,如果我们需要修改某一个静态属性,则需要自己获取descriptor
  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

Decorator 用例

log

为一个方法添加 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);

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);
}
}
}
}
}

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

更多关于 core-decorators 的例子后面再

Nealyang/PersonalBlog中补充,再加注释说明。

参考

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。

公众号内回复 【1】,加入全栈前端学习群,一起交流。

Decorator:从原理到实践的更多相关文章

  1. Atitit 管理原理与实践attilax总结

    Atitit 管理原理与实践attilax总结 1. 管理学分类1 2. 我要学的管理学科2 3. 管理学原理2 4. 管理心理学2 5. 现代管理理论与方法2 6. <领导科学与艺术4 7. ...

  2. Atitit.ide技术原理与实践attilax总结

    Atitit.ide技术原理与实践attilax总结 1.1. 语法着色1 1.2. 智能提示1 1.3. 类成员outline..func list1 1.4. 类型推导(type inferenc ...

  3. Atitit.异步编程技术原理与实践attilax总结

    Atitit.异步编程技术原理与实践attilax总结 1. 俩种实现模式 类库方式,以及语言方式,java futuretask ,c# await1 2. 事件(中断)机制1 3. Await 模 ...

  4. Atitit.软件兼容性原理与实践 v5 qa2.docx

    Atitit.软件兼容性原理与实践   v5 qa2.docx 1. Keyword2 2. 提升兼容性的原则2 2.1. What 与how 分离2 2.2. 老人老办法,新人新办法,只新增,少修改 ...

  5. Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法

    Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...

  6. Atitit.gui api自动化调用技术原理与实践

    Atitit.gui api自动化调用技术原理与实践 gui接口实现分类(h5,win gui, paint opengl,,swing,,.net winform,)1 Solu cate1 Sol ...

  7. Atitit.提升语言可读性原理与实践

    Atitit.提升语言可读性原理与实践 表1-1  语言评价标准和影响它们的语言特性1 1.3.1.2  正交性2 1.3.2.2  对抽象的支持3 1.3.2.3  表达性3 .6  语言设计中的权 ...

  8. Atitit 网络爬虫与数据采集器的原理与实践attilax著 v2

    Atitit 网络爬虫与数据采集器的原理与实践attilax著 v2 1. 数据采集1 1.1. http lib1 1.2. HTML Parsers,1 1.3. 第8章 web爬取199 1 2 ...

  9. Atitit.软件兼容性原理与实践 v3 q326.docx

    Atitit.软件兼容性原理与实践 v3 q326.docx 1. 架构兼容性1 2. Api兼容性1 2.1. 新api  vs  修改旧的api1 3. Web方面的兼容性(js,html)1 3 ...

随机推荐

  1. C#中appium自动化执行移动命令mobile:shell用法

    官网:https://appium.readthedocs.io/en/latest/en/commands/mobile-command/#android 1.执行ADB shell命令(需要设置服 ...

  2. 逆向破解之160个CrackMe —— 016

    CrackMe —— 016 160 CrackMe 是比较适合新手学习逆向破解的CrackMe的一个集合一共160个待逆向破解的程序 CrackMe:它们都是一些公开给别人尝试破解的小程序,制作 c ...

  3. 如何使用python records 库优雅的操作数据库

    今天要介绍的这个python第三方库非常厉害,完美操作各种数据库.名字叫 records, 在网上很少有这个库的相关资料,但是在开源社区可是很火热的哦.如果这还不能打消你的顾虑,再告诉你一件事:如果你 ...

  4. 讲解开源项目:功能强大的 JS 文件上传库

    本文作者:HelloGitHub-kalifun HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...

  5. Springboot源码分析之@Transactional

    摘要: 对SpringBoot有多了解,其实就是看你对Spring Framework有多熟悉~ 比如SpringBoot大量的模块装配的设计模式,其实它属于Spring Framework提供的能力 ...

  6. python学习——列表和元组

    一.列表 1)列表介绍 列表是Python内置的一种数据类型. >一组有序项目的集合(从第一个成员序号为0开始依次递增排序) >可变的数据类型(可进行增删改查) >列表中可以包含任何 ...

  7. .NET平台下,钉钉微应用开发之:工作消息通知

    首先看下官方文档,为我们提供了POST请求地址,和几个必传参数的列表以及参数示例,写的都挺详细的. 无奈提供的SDK请求示例是JAVA的,而我用的是.NET的,所以还是摸了一些坑出来,其实也就是不同平 ...

  8. spring-cloud-kubernetes的服务发现和轮询实战(含熔断)

    本文是<spring-cloud-kubernetes实战系列>的第四篇,主要内容是在kubernetes上部署两个应用:Web-Service和Account-Service,通过spr ...

  9. 牛客练习赛22C Bitset

    牛客练习赛22C 一共有 n个数,第 i 个数是 xi  xi 可以取 [li , ri] 中任意的一个值. 设 ,求 S 种类数. 感觉二进制真是一个神奇的东西. #include <iost ...

  10. ASP.NET CORE Docker发布记录

    1.安装Docker yum install curl -y curl -fsSL https://get.docker.com/ | sh 2.编写Dockerfile文件 FROM microso ...