当你们都再聊要不要学习框架的时候,笔者却还在学规范,当标题党。本文的一切,源于网络,感恩开源的世界...javascript
虽然本文的初衷是讲 ES7 中的装饰器,但笔者更喜欢在探索的过程当中加深对前端基础知识的理解。本着一颗刨根问底儿的心,分享内容会尽量多地将一些关联知识串联起来说解。html
乍一看可能会有点乱,但倒是笔者学习一个新知识的完整路径。 一种带着关键词去学习的方法,比较笨,读者选读便可,取精华去糟粕。前端
另外,这个仓库 是专门用来记录 Decorators 低侵入性探索 收获的知识。后续可能会结合 mobx 源码、以及在 React 中实际应用场景来深刻。vue
前端知识广度一望无际,深度深不可测,笔者记性很差,相似的仓库有:java
Decorators 属于 ES7, 目前处于提案阶段,可经过 babel
或 TS
编译使用。git
本文属于探索型,主要分为三部分:github
Decorators 基础知识npm
Babel 与 TypeScript 支持json
常见应用场景数组
装饰器 (Decorators) 让你能够在设计时对类和类的属性进行“注解”和修改。
Decorators
通常接受三个参数:
目标对象 target
属性名称 key
描述对象 descriptor
可选地返回一个描述对象来安装到目标对象上,其的函数签名为
function(target, key?, descriptor?)
。
Decorators
的本质是利用了 ES5 的 Object.defineProperty
方法,这个方法着实改变了不少,好比 vue 响应式数据的实现方法,固然还有更为迷人 proxy
,是否是发现,不少框架背后的靠山都离不开这些底层规范的支持。
下面来简单了解下这个方法:
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty(obj, prop, descriptor)
obj
要在其上定义属性的对象。
prop
要定义或修改的属性的名称。
descriptor
将被定义或修改的属性描述符。
返回值
被传递给函数的对象。
其中 descriptor
可经过 Object.getOwnPropertyDescriptor()
方法得到。
Object.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor()
方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不须要从原型链上进行查找的属性)
obj
须要查找的目标对象
prop
目标对象内属性名称(String 类型)
返回值
若是指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),不然返回 undefined
。
一个属性描述符是一个记录,由下面属性当中的某些组成的:
value
该属性的值(仅针对数据属性描述符有效)
writable
当且仅当属性的值能够被改变时为 true
。(仅针对数据属性描述有效)
configurable
当且仅当指定对象的属性描述能够被改变或者属性可被删除时,为 true
。
enumerable
当且仅当指定对象的属性能够被枚举出时,为 true
。
get
获取该属性的访问器函数(getter
)。若是没有访问器, 该值为 undefined
。(仅针对包含访问器或设置器的属性描述有效)
set
获取该属性的设置器函数(setter
)。 若是没有设置器, 该值为 undefined
。(仅针对包含访问器或设置器的属性描述有效)
各式的装饰器通常都是基于修改上述属性来实现,好比 writable
可用于设置 @readonly
。更多的功能,可参考 lodash-decorator
如今咱们对 Decorators 方法 function(target, key?, descriptor?)
混了个脸熟,同时知道了Object.defineProperty
和 Descriptor
与 Decorators 的联系。
可是,目前浏览器对 Es7 这一特性支持 并不友好。Decorators 目前还只是语法糖,尝鲜可经过 babel 、TypeScript。
接下来就来了解这一部分的内容。
不少构建工具都离不开 babel,好比笔者用于快速跑 demo 的 parcel。虽然不少时候咱们并不须要关心这些构建后的代码,但笔者建议有时间仍是多了解下,毕竟前端打包后出现的 bug 仍是很常见的。
回到装饰器,现阶段官方说有 2 种装饰器,但从实际使用上可分为 4 种,分别是:
“类装饰器” 做用于 class
。
“属性装饰器” 做用于属性上的,这须要配合另外一个的类属性语法提案,或者做用于对象字面量。
“方法装饰器” 做用于方法上。
“访问器装饰器” 做用于 getter
或 setter
上的。
下面咱们经过 babel 命令行,来感觉一下各装饰器:
先简单介绍下 babel 的用法:
babel
npm i -g babel
复制代码
.babelrc
{
"presets": [["es2015", { "modules": false }]],
"plugins": ["transform-decorators-legacy", "transform-class-properties"],
"env": {
"development": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
}
复制代码
package.json
配置 npm script{
"babel": "babel ./demo/demo.js -w --out-dir dist"
}
复制代码
该命令的意思是:监听 demo
目录下 demo.js
文件,并将编译结果输出到 dist
目录
下面列出各装饰器在 babel
编译后对应的输出结果。
从编译后的结果能够看到,autobind
做为装饰器只接受了一个参数,也就是类自己(构造函数)。
class MyClass = {}
MyClass = autobind(MyClass) || MyClass
复制代码
bebel 对于方法装饰器
的处理会比较特别,下面看下核心处理:
var _class;
// 一、首先,初始化一个 class
var initClass = (_class = (function() {
// ... 类定义
})());
// 二、经过 `_applyDecoratedDescriptor` 方法使用传入的装饰器对 `_class.prototype` 中的方法进行装饰处理。
var Decorator = _applyDecoratedDescriptor(
_class.prototype,
'getName',
[autobind],
Object.getOwnPropertyDescriptor(_class.prototype, 'getName'),
_class.prototype
);
// 三、利用逗号操做符的做用,返回装饰完的 `_class`
var MyClass = (initClass, Decorator, _class);
复制代码
后续会对 _applyDecoratedDescriptor
方法进一步讲解。
逗号操做符 对它的每一个操做数求值(从左到右),并返回最后一个操做数的值。
“访问器装饰器” 的处理方式与 “方法装饰器”相似。
区别在于传入的第三个参数 Descriptor
并非由 Object.getOwnPropertyDescriptor(_class.prototype, 'getName')
返回的,而且多了一个 Descriptor
上并不存在的 initializer
属性供 _applyDecoratedDescriptor
方法使用。
_applyDecoratedDescriptor(
_class.prototype,
'getName',
[autobind],
// Object.getOwnPropertyDescriptor(_class.prototype, 'getName'),
{
enumerable: true,
initializer: function initializer() {
return function() {};
}
}
))
复制代码
接下来就让咱们来看一下 _applyDecoratedDescriptor
都作了哪些事
_applyDecoratedDescriptor
_applyDecoratedDescriptor
实际上是对 decorator
的一个封装,用于处理多种状况。其接受的参数跟 decorator
大致一致。
target
目标对象
property
属性名称
descriptor
属性描述对象
decorators
装饰器函数 (数组,表示可传入多个装饰器)
context
上下文
返回值
属性描述对象
function _applyDecoratedDescriptor( target, property, decorators, descriptor, context ) {
// 一、经过传入参数 `descriptor` 初始化最终导出的 `属性描述对象`
var desc = {};
Object['ke' + 'ys'](descriptor).forEach(function(key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
// 二、存在 `value` 或者 class 初始化属性 则将 `writable` 设置为 `true`
if ('value' in desc || desc.initializer) {
desc.writable = true;
}
// 三、处理传入的 decorator 函数
// 其中 `reverse` 保证了,当同一个方法有多个装饰器,会由内向外执行。
desc = decorators
.slice()
.reverse()
.reduce(function(desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
// 看 babel 编译后的代码,当 `initializer` 不为 `undefined` 时,并不会传入 `context`
// 笔者看不懂! ??? 这是一个永远不会执行的逻辑... 难道改走 `_initDefineProp` 逻辑了?
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
// 4. 使用 Object.defineProperty 对 `target` 对象的 `property` 属性赋值为 `desc`
if (desc.initializer === void 0) {
Object['define' + 'Property'](target, property, desc);
desc = null;
}
return desc;
}
复制代码
void 运算符 对给定的表达式进行求值,而后返回
undefined
。
如今咱们对 Descorators
有了大体的了解,接下来看下 Descorators 基于 babel 编译下的装饰器
this
咱们先来看一个关于 this
的问题
this
的指向问题class Person {
getPerson() {
return this;
}
}
let person = new Person();
const { getPerson } = person;
getPerson() === person; // false
person.getPerson() === person; // true
复制代码
这段代码中,getPerson
和 person.getPerson
指向同一个函数且返回 this
,但它们的执行结果却不同。
this
指的是函数运行时所在的环境:
getPerson()
运行在全局环境,因此 this
指向全局环境
person.getPerson
运行在 person
环境,因此 this
指向 person
关于 this
的原理能够参考 这篇:
在本例中,getPerson()
是一个函数,JavaScript 引擎会将函数单独保存在内存中,而后再将函数的地址赋值给 getPerson
属性的 value
属性 (descriptor)
因为函数单独存在于内存中,因此它能够在不一样的环境 (上下文) 执行。
来看个例子:
// 注意,这里都是用 var 声明变量
var name = 'globalName';
var fn = function() {
console.log(this.name);
return this.name;
};
var person = {
getPerson: fn,
name: 'personName'
};
// 单独执行
var ref = person.getPerson;
ref();
// or
fn();
// person 环境指执行
person.getPerson();
复制代码
函数能够在不一样的运行环境 (context),因此须要一种机制,可以在函数体内部得到当前的运行环境。
这里 this
的设计目的就是在函数体内部,指代函数当前的运行环境。
例子中,fn()
和 ref()
的运行环境都是 全局运行环境 而 person.getPerson()
的运行环境是 person
,所以获得了不一样的 this
解决 this
指向的方法有不少种,好比函数的原型方法
经过上面学习到的知识,接着来说解 Decorator
中如何实现 autobind
给函数或类自动绑定 this
1、 首先来看下 如何给类的方法自动绑定 this
:
var obj = {
fn: function() {
console.log('执行时的', this);
}
};
var fn = Object.getOwnPropertyDescriptor(obj, 'fn').value;
Object.defineProperty(obj, 'fn', {
get() {
console.log('get 访问器里的', this);
return fn;
}
});
var fn = obj.fn;
fn();
obj.fn();
复制代码
能够获得的一个结论:get(){}
访问器属性里面的 this
始终指向 obj
这个对象。
若是简化逻辑,也就是不考虑其余特殊状况下,autobindMethod
应该是这样的:
function autobindMethod(target, key, { value: fn, configurable, enumerable }) {
return {
configurable,
enumerable,
get() {
const boundFn = fn.bind(this);
defineProperty(this, key, {
configurable: true,
writable: true,
enumerable: false,
value: boundFn
});
return boundFn;
},
set: createDefaultSetter(key)
};
}
复制代码
bind() 方法建立一个新的函数, 当这个新函数被调用时 this 键值为其提供的值,其参数列表前几项值为建立时指定的参数序列。
有了 autobind
这个装饰器,getName
方法的 this
就始终指向实例对象自己了。
class TestGet {
@autobind
getName() {
console.log(this);
}
}
复制代码
2、接着来看下类的 autobind
实现
对类绑定 this
其实就是为了批量给类的实例方法绑定 this
因此只要获取全部实例方法,再调用 autobindMethod
便可。
function autobindClass(klass) {
const descs = getOwnPropertyDescriptors(klass.prototype);
const keys = getOwnKeys(descs);
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
const desc = descs[key];
if (typeof desc.value !== 'function' || key === 'constructor') {
continue;
}
defineProperty(
klass.prototype,
key,
autobindMethod(klass.prototype, key, desc)
);
}
}
复制代码
以上实现考虑的是 Babel 编译后的文件,除了 Babel ,TypeScript 也支持编译 Decorators。
所以就须要一个更为通用的 Decorators 包装函数,接下来让咱们一块儿实现它。
先来一块儿看下 TypeScript 编译后的结果。
从上图能够看出,TypeScript 对 Decorator 编译的结果跟 Babel 略微不一样,TypeScript 对属性和方法没有过多的处理,惟一的区别可能就是在对类的处理上,传入的 target
为类自己,而不是 Prototype
。
不管是用什么编译器生成的代码,最终参数仍是离不开 target, name, descriptor
。另外,不管怎么包装,最终也是为了提供一个可以新增或者修改 descriptor
某个属性的函数,只要是对属性的修改,就必然离不开 Object.defineProperty
。
有时候,咱们难以读懂某段代码,可能只是由于没有进入这段代码的真实上下文(应用场景)。若是是按需求来开发某个 Decorator,事情就会变得简单。
通用 Decorator,意味着将要用于生成具备共有特征且用于不一样场景的装饰器,一般最容易让人想到就是工厂模式。
咱们来看下 lodash-decorators
中的实现:
export class InternalDecoratorFactory {
createDecorator(config: DecoratorConfig): GenericDecorator {
// 基础装饰器
}
createInstanceDecorator(config: DecoratorConfig): GenericDecorator {
// 生成用于实例的装饰器
}
private _isApplicable(
context: InstanceChainContext,
config: DecoratorConfig
): boolean {
// 是否可调用
}
private _resolveDescriptor(
target: Object,
name: string,
descriptor?: PropertyDescriptor
): PropertyDescriptor {
// 获取 Descriptor 的通用方法。
}
}
复制代码
这里用 TypeScript 的好处在于,类自己具有某种结构。也就是可供类型描述使用。另外,在看源码过程当中,TypeScript 的类型有助于快速理解做者意图。
好比单看上面代码,咱们就能够知道 createDecorator
和 createInstanceDecorator
都接收类型为 DecoratorConfig
的参数,以及返回都是通用的 Decorator GenericDecorator
。
那咱们先来看下:
export interface DecoratorConfigOptions {
bound?: boolean;
setter?: boolean;
getter?: boolean;
property?: boolean;
method?: boolean;
optionalParams?: boolean; // 是否使用自定义参数
}
export class DecoratorConfig {
constructor( public readonly execute: Function, // 处理函数,如传入 debounce 函数 public readonly applicator: Applicator, // 根据处理函数不一样,选用不一样的函数调用程序。 public readonly options: DecoratorConfigOptions = {} ) {}
}
复制代码
关键的参数有:
execute
装饰函数的核心处理函数。applicator
主要做用是用于配置参数及函数的调用。options
额外的配置选项,如是不是属性,是不是方法,是否使用自定义参数等。这里的 Applicator 属于函数调用中公共部分的抽离:
export interface ApplicateOptions {
config: DecoratorConfig;
target: any;
value: any;
args: any[];
instance?: Object;
}
export abstract class Applicator {
abstract apply(options: ApplicateOptions): any;
}
复制代码
一个通用的 Decorator 的核心部分差很少就这些了,但因为笔者实际应用 Decorators 的地方很少,对于 lodash-decorators
源码中为何有 createDecorator
和 createInstanceDecorator
两种生成方法,以及为何要引入 weekMap
的缘由,一时也给不了很是准确的答案。createInstanceDecorator
也许是出于原型链考虑?由于实例,才能访问原型链继承后获得的方法,之后有机会再单独深刻。
但愿有这方面研究的读者能够不吝赐教,笔者不胜感激。
结合 lodash
,关注点分离了。实现各类 decorators 在代码实现上就变得很是简单。好比,前端可能会常常用到的函数节流,函数防抖,delay。
import debounce = require('lodash/debounce');
import { PreValueApplicator } from './applicators';
const decorator = DecoratorFactory.createInstanceDecorator(
new DecoratorConfig(debounce, new PreValueApplicator(), { setter: true })
);
export function Debounce( wait?: number, options?: DebounceOptions ): LodashDecorator {
return decorator(wait, options);
}
复制代码
经过调用 DecoratorFactory
生成通用的 decorator,实现各类装饰器功能就只须要像上面同样组织代码便可。
另外像 Mixin
这种看似组合优于继承的用法是一种对类的装饰,能够这么去实现:
import assign = require('lodash/assign');
export function Mixin(...srcs: Object[]): ClassDecorator {
return ((target: Function) => { assign(target.prototype, ...srcs); return target; }) as any; } 复制代码
更多的功能,笔者就再也不过多赘述。再讲就变成 lodash 源码解析了。有心的读者能够去触类旁通了,或者直接看 lodash-decorators
源码。毕竟我也是看它们源码来学习的。
这么草率的结束,也许意味着还有更多学习空间。
Decorators
涉及的知识并不难,关键在于如何巧妙运用。初期没经验,能够学习笔者看些周边库,好比 lodash-decorators
。所谓的低侵入性,也只是视觉感官上的,不过确实多少能提升代码的可读性。
最后,前端路上,多用 【闻道有前后,术业有专攻】安慰本身,学习永无止境。 感谢阅读,愿君多采撷!