0. 引言
平时我们用
decorator
来封装一些和原有类或者
react
组件(高阶组件)本身无关的功能。比如说埋点、路由、hack、复杂冗余的业务逻辑、以及扩展的功能等,非常好用。本文就怎么使用
decorator
,以及如何扩展及应用场景做下简单总结。
1. 准备工作
安装
babel
转码。
npm install --save-dev babel-cli babel-plugin-transform-decorators-legacy
根目录配置.babelrc
{
"presets": ["env"],
"plugins": ["transform-decorators-legacy"]
}
package.json:
// npm init 后的package.json
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
// 写入
"build": "./node_modules/.bin/babel 你的原es6.js -o 转成的es5.js"
// 目录写法
// "build": "./node_modules/.bin/babel src -d lib"
},
"devDependencies": {
"babel-cli": "^6.0.0"
}
}
babel-cli安装在全局的话,就不需要按照上面在以上script中添加build了。
运行:
npm run build // 全局babel-cli 使用 babel 你的原es6.js > 转成的es5.js
即可生成可运行的es5文件,然后使用
node file.js
在node里运行,或者引入html中即可。
当然也可以直接在
babel网站
中编辑转码。
decorator
可以装饰
对象
和
属性
,下面就分开介绍。
2. decorator 装饰属性
装饰属性,会在属性注册到类prototype之前先执行装饰器,先看源码怎么写。
// 定义一个装饰器函数,里面的target这些参数和Object.defineProperty是对应的
function ready(target, name, descripter) {
descripter.writable = false; // 改写writable属性
return descripter; // 注意,返回属性描述对象descriptor
}
class A() {
@ready
b() {
console.log('f');
}
}
再看babel转义成es5的核心代码:
function applyDecorator(target, property, decorators, descriptor, context) {
// 定义一个新的描述对象
var desc = {};
// 将target.property 的descriptor挂载在desc上
// 从后往前依次覆盖前面的desc上的属性
desc = decorators.slice().reverse().reduce(function(desc, decorator) {
return decorator(target, propery, desc) || desc;
});
return desc;
}
applyDecorator(A.prototype, 'b', [ready], Object.getOwnPropertyDescriptor(A.prototype, 'b'), A.prototype);
经过ready处理后的descriptor 返回到类
A(Class)
的原型上的属性
b
上,之后对
b
方法的重写将会被禁止,这样我们可以针对
b
方法进行一些限制、拦截和改造性质的操作,包括
get
、
set
方法。
另外,这里的
reduce
用的非常好。将含有处理函数的数组倒置,,然后使用
reduce
,每次处理函数返回值作为下一次的第一个属性再次传入,下一个处理函数继续处理返回值,
redux
库插件处理state就是这种思路。
3. decorator 装饰对象
装饰对象是大家使用decorator最广泛的场景。
function filter(flag) {
return function(target) {
// handler flag
// 也可以写一些自定义的方法挂载在target上
target.getName = function() {
return 'wf'
};
}
}
@filter(true)
class A {
constructor() {
this.name = "my"
}
getName() {
return this.name;
}
}
const a = new A();
console.log(a.getName()); // wf
最终转化成es5,直接传入 A,内部改写A的方法。
A = filter(true)(A);
也就是说,装饰器通过传入对象或者对象及方法名,通过改写他的行为。
改写可以通过
Object.defineProperty
,也可以通过
target.otherName
的方式,定义和赋值的区别,
defineProperty
这种能设置属性的特性,限制扩展等。而直接赋值会受到访问器属性的
get``set
的影响,也会影响访问器属性。
这样我们就能在上面能访问内部的属性,同时也可以添加许多扩展性的功能,这里不作扩展说明,后续设计模式类的文章会介绍。
这里·额外·说下babel转码后的代码里有 这种括号
const a = (m, n, v)
运算符。
var a = function(v) {
return function(v){
return v + 'v';
}
}
var m;
var b = (m = a('v'), m('wf'), ''); // ''
以上代码最终返回空, ()里会依次从前到后计算,最后返回最后一个值!!! 是不是有点像reduce,不过人家无法传递参数,只能指定固定的表达式。
4. 实践中引入decorator
下面我们开始使用
decorator
去实践一些东西。比如以形容我们(程序员)为例, 哈哈哈。
我们先建立一个
基类
,从一个错误的示例开始(熟悉原型链的可以直接略过),了解下日常写
decorator
的坑:
class Programmer {
constructor(hasGirlF) {
this.hasGirlF = !!hasGirlF; // 女朋友
}
}
下面来实例化并为他添加特殊属性:
function fallInLove(flag = false) {
return function(target) {
target.hasGirlF = flag;
}
}
@fallInLove(true) // 嘿嘿
class Programmer {
constructor(hasGirlF) {
this.hasGirlF = !!hasGirlF;
}
}
const wf = new Programmer();
wf.hasGirlF; // false ???
Programmer.hasGirlF; // true
为什么改为true了,结果还是没有女朋友!!!
因为上面也有提到,
decorator
传入的
target
其实就是
Programmer
这个类,
target.hasGirlF
其实修改的是这个类的静态属性,而不是实例化后的
wf
。
因此,这里需要改进,
class
中定义的方法其实就是该类的原型方法,我们可以尝试为原型添加属性:
function fallInLove(flag = false) {
return function(target) {
target.prototype.hasGirlF = flag; // 注意prototype
}
}
@fallInLove(true) // 嘿嘿
class Programmer {
constructor(hasGirlF) {
this.hasGirlF = !!hasGirlF; // 未定义hasGirlF为false
}
}
const wf = new Programmer();
wf.hasGirlF; // false ???
Programmer.hasGirlF; // undefined
Programmer.prototype.hasGirlF; // true
还是不正常。其实是因为根据原型链继承的思想,先查找实例中的
hasGirlF
,再沿着原型链往上找。实例中有
hasGirlF
了,就不会往原型链上找了。
对此总结下上面的错误
:
-
es6
类的
decorator
中传入的
target
是类本身,而不是它的原型,所以直接在上面添加方法是无法被实例引用到的 -
不要在
decorator
中添加和个体(实例)相关的属性,因为修改原型会影响到每个实例,并且很有可能被构造函数覆盖
开始正确的示例,先给大家添加点共性的属性:
function addProps(...props) {
return function(target) {
(target.prototype.props = []).push(...props);
}
}
@addProps('聪明')
class Programmer {
constructor(hasGirlF) {
this.hasGirlF = !!hasGirlF;
}
fallInLove(flag = false) {
this.hasGirlF = flag;
}
}
const wf = new Programmer();
wf.props; // 聪明
const my = new Programmer();
my.props; // 聪明 bingo!
以上能用
addProps
给所有实例添加共有的属性
聪明
,同时通过实例方法
fallInlove
也能设置实例自身
hasGillF
,保证了可复用和扩展。
5. 使用mixin扩展decorator
以上,一个新增的属性通过装饰器
addProps
添加到
Programmer
类上去了。
但是,如果想添加、覆盖一堆新方法,或者想复制另一类的方法,那这一个个地添加岂不是很麻烦。
这个时候, 我们需要用到
mixin
和
extends
。下面我们先通过
mixin
来复制其他对象的行为。
const mixin = (behaviour) =>
target => {
Object.assign(target.prototype, behaviour);
return target; // 一定要return target 不能返回prototype,其为对象
}
@mixin({
props: [],
addProps: function(...props) {
this.props.push(...props);
}
})
class Programmer {
constructor(hasGirlF) {
this.hasGirlF = !!hasGirlF;
}
fallInLove(flag = false) {
this.hasGirlF = flag;
}
}
const wf = new Programmer();
wf.props; // []
wf.addProps('乐观');
wf.props; // ['乐观']
这样,
Programmer
类就能使用
mixin
中调用的类和方法了。
但是这里mixin中引入对象的行为都是
可枚举的
,为了让
mixin
功能更贴近
class
,这里存在两个小问题:
-
真正的es6的
class
中定义的行为是不可枚举的 -
Object.assign
只会复制可枚举的属性和方法
在此我们转换下,使其和
class
一致:
const mixin = (behaviour) =>
target => {
for (let property of Reflect.ownKeys(behaviour)) { // ownKeys相比Object.keys能遍历出class中方法(不可枚举属性)
Object.defineProperty(target.prototype, property, { value: behaviour[property] })
}
return target;
}
6. 使用extends代替decorator
其实上面能做到的,
extends
都能做到,而且更加透明易懂。
class Behavior extends Programmer {
addProps: function(...props) {
this.props.push(...props);
}
}
搞定!!!
extends
能轻松地搞定这些继承问题,是不是感觉上面
mixin
结合
decorator
的写法很鸡肋?
但是
,在某些情况下,这里使用
mixin
和
extends
都有不足,那就是无论是哪种方法,因为合成(mixin)和继承(extend),都会有一个类被影响,丧失了原有的纯粹性:
-
mixin
覆盖和新增了
Programmer
中原型的方法 -
extends
使
Behaviour
需要带入
Programmer
中的方法,使其无法被其他类型的类复用
这里我们用一种新的方式取代:
const mixin = (target, Behaviour) => {
const newTarget = class extends target {}
for (let property of Reflect.ownKeys(Behaviour)) {
Object.defineProperty(newTarget.prototype, property, { value: Behaviour[property] })
}
return newTarget;
}
const newBehavior = mixin(Programmer, Behaviour),
以上,通过在
mixin
内部定义一个类继承
Programmer
类,再把
Behaviour
的方法复制到它上面,
Behaviour
这个类没有混入
Programmer
中的方法,
Programmer
也没有被
Behaviour
的方法覆盖,这样
Programmer
和
Behaviour
本身都不会被影响,同时又合成了一个共有属性的新类,基于此,
decorator
能实现的以上都可以实现,利用
decorator
可以这样写。
const compose = (Behaviour) => (Programmer) => mixin(Programmer, Behaviour);
@compose(behaviour)
class OtherClass {}
个人认为,以上方法比较适用于已有代码扩展,在两个类都保持独立的情况下添加额外的方法来集成,不同的应用场景下选择是各不相同的。很多
decorator
直接通过
extends
写更方便,不需要外面再套一层
decorator
,再在
decorator
中
extends
,但是
decorator
存在的优势就是
mixin
作为函数的存在,其中传入对象参数的时候更灵活,可以实现
多重继承
和
自定义
。另外,像
依赖链
、
不可见
、
复杂性
这种不足,其实
extends
和
mixin
是差不多的。
7. decorator引入React
react
的
decorator
其实是利用高阶组件(HOC)来完成的,这里使用PP(Props Proxy)作为例子。(关于HOC的文章很多,可以看看
这篇
)
const Log = (WrappedCompoent) => class extends React.Component {
// 扩展的业务逻辑
// 可访问App的this
render() {
return (<WrappedComponent
otherProps={this.otherProps}
{...this.props} />);
}
}
@Log
class App extends React.Component {
// 业务逻辑
constructor(props) {
super(props);
// 可以取到this.props.otherProps
}
}
这里Log传入的就是App类,在不影响App组件的正常情况下,App中还可以获取高阶组件中定义的方法和属性,同时高阶组件也能做额外的一些事情,特别方便。
参考文献
-
Functional Mixins in ECMAScript 2015.raganwald
作者的文章写的非常好!