1、先决条件
因此本文只阐述类和方法的装饰器
2、项目配置
本文的项目配置将会采用笔者在之前的文章中所用到的配置。从babel编译结果来学习ES6-class
3、类的装饰
3.1、 基础
我们来编写一个简单的装饰器
function Wrap(target) {
return target;
}
@Wrap
class Polygon {}
运行
npm run build
我们将会得到如下结果:
"use strict";
var _class;
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function Wrap(target) {
return target;
}
var Polygon =
Wrap(
(_class = function Polygon() {
_classCallCheck(this, Polygon);
})
) || _class;
从babel转码的结果,我们可以知道,如果类装饰器不返回内容,将不会对类进行修改。并且我们还可以知道,如果在别的地方引用此类的话,类装饰器已经执行了。如果类装饰器返回内容的话(且为真),那么我们的这个类就被改写了。实际例子:vue-class-component的Component装饰器。
3.2、多个装饰器装饰器同一个类
后续的操作如上,将不会赘述。 源码:
function Wrap(target) {
return target;
}
function Component(target) {
return target;
}
function Decs(target) {
return target;
}
@Wrap
@Component
@Decs
class Polygon {}
转码结果:
function Wrap(target) {
return target;
}
function Component(target) {
return target;
}
function Decs(target) {
return target;
}
var Polygon =
Wrap(
(_class =
Component(
(_class =
Decs(
(_class = function Polygon() {
_classCallCheck(this, Polygon);
})
) || _class)
) || _class)
) || _class;
从这个结果,我们可以得出结论,离class越近的装饰器越先执行。
3.3 接受参数的装饰器
当我们的类装饰器需要获得参数的时候,在装饰器外面再封装一层函数。 我们先来一个错误的示范,各位读者请看,此时Component并没有写成函数的执行形式,我们来看转码结果: 源码:
function Wrap(target) {
return target;
}
function Component(options = {}) {
return function (target) {
return target;
};
}
function Decs(target) {
return target;
}
@Wrap
@Component
@Decs
class Polygon {}
转码结果:
function Wrap(target) {
return target;
}
function Component() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return function (target) {
return target;
};
}
function Decs(target) {
return target;
}
var Polygon =
Wrap(
(_class =
Component(
(_class =
Decs(
(_class = function Polygon() {
_classCallCheck(this, Polygon);
})
) || _class)
) || _class)
) || _class;
从转码结果来看,因为我们的粗心,此时Component返回的是function (target) { return target; };后面的装饰器所接收的结果均会是这个function,这样,我们的原始类已经被修改了。如果我们的类装饰器需要接收参数,一定要写成函数的执行形式,否则会给程序造成潜在的bug,这个小坑大家一定要注意。 笔者喜欢一个设计API的原则——恶心自己,成全别人。这句话的意思就是说,在设计API的时候给用户尽可能的提供提供便利性,而在设计API的时候增加很多额外的判断,这大概是老祖宗流传下来的哲学吧。
function anonymous(target) {
return target;
}
function Component() {
var isFunc = arguments.length > 0 && typeof arguments[0] === 'function';
return isFunc ? anonymouse(arguments[0]) : anonymous;
}
容错能容的了一时,但是容不了一世,如:
@Component(function(){ })
class Polygon {}
此时我们的程序就傻眼了,反而还出现了bug,因此,对于我们开发者来说,更多的还是要仔细阅读API才是最终的解决方案。 正确编写可接受参数的装饰器之后,转码的结果:
var Polygon =
((_dec = Component({
age: "26",
name: "hsuyang",
})),
Wrap(
(_class =
_dec(
(_class =
Decs(
(_class = function Polygon() {
_classCallCheck(this, Polygon);
})
) || _class)
) || _class)
) || _class);
4、 方法或属性装饰器
其中descriptor的描述如下:
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
由于装饰器在装饰属性和方法上还是有一定的区别的,因此笔者把这里单独拆开讲
4.1 属性装饰器
属性装饰器babel对于属性的初始化,运用了一个叫initializer的方法(笔者最开始学习的时候也是比较懵逼,难道阮一峰老师错了,结果阮一峰老师没有错,只是自己的见识还不够?)。 源码:
function readonly(target, name, descriptor) {
descriptor.writable = false;
descriptor.value = function () {
console.log("hsuyang");
};
return descriptor;
}
class Person {
@readonly
name1;
@readonly
name2 = "I love microsoft";
}
转码结果:
"use strict";
var _class, _descriptor, _descriptor2, _temp;
function _initializerDefineProperty(target, property, descriptor, context) {
if (!descriptor) return;
Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 });
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
var desc = {};
Object.keys(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;
}
desc = decorators
.slice()
.reverse()
.reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
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.defineProperty(target, property, desc);
desc = null;
}
return desc;
}
function _initializerWarningHelper(descriptor, context) {
throw new Error("Decorating class property failed. Please ensure that " + "proposal-class-properties is enabled and runs after the decorators transform.");
}
function readonly(target, name, descriptor) {
descriptor.writable = false;
descriptor.value = function () {
console.log("hsuyang");
};
return descriptor;
}
var Person =
((_class =
((_temp = function Person() {
_classCallCheck(this, Person);
_initializerDefineProperty(this, "name1", _descriptor, this);
_initializerDefineProperty(this, "name2", _descriptor2, this);
}),
_temp)),
((_descriptor = _applyDecoratedDescriptor(_class.prototype, "name1", [readonly], {
configurable: true,
enumerable: true,
writable: true,
initializer: null,
})),
(_descriptor2 = _applyDecoratedDescriptor(_class.prototype, "name2", [readonly], {
configurable: true,
enumerable: true,
writable: true,
initializer: function initializer() {
return "I love microsoft";
},
}))),
_class);
代码虽然多,但是核心就在于_applyDecoratedDescriptor和_initializerDefineProperty这两个方法。
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
var desc = {};
Object.keys(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;
}
//应用多个装饰器
desc = decorators
.slice()
.reverse()
.reduce(function (desc, decorator) {
// 如果装饰器不返回任何东西,则默认采用这个descriptor,因为JS语言的特性,desc是一个对象,当我们把这个对象传递到函数内部执行之后,这个对象已经被修改了,因此,属性装饰器,其实不管是否return descriptor,效果都是一样的。
return decorator(target, property, desc) || desc;
}, desc);
// initializer 是专门用来处理在class中对 属性或者方法进行赋值语句操作的
// 如果赋值语句存在,则属性的值等于赋值语句的结果
if (context && desc.initializer !== void 0) {
//并且把赋值上下文的this绑定进去
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
//当处理完initializer之后,被处理成了undefined。这很重要
desc.initializer = undefined;
}
//如果,没有赋值语句,则按照普通的Object.defineProperty处理。对于Object.defineProperty不懂的读者请移步MDN。
if (desc.initializer === void 0) {
Object.defineProperty(target, property, desc);
desc = null;
}
return desc;
}
function _initializerDefineProperty(target, property, descriptor, context) {
if (!descriptor) return;
Object.defineProperty(target, property, {
enumerable: descriptor.enumerable,
configurable: descriptor.configurable,
writable: descriptor.writable,
//请各位看官看仔细了,对,你确定你没有看错,如果对于没有赋值的属性,你如果想通过装饰器赋值,是不能赋值的。
value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
});
}
各位尤其要注意这个_initializerDefineProperty方法,想要通过属性装饰器,通过修改descriptor.value,给属性设置值,是行不通的。因为之前在处理initializer的时候,处理完之后,已经被设置成了undefined,你想要通过initializer去改变属性的值,也是徒劳的。
还有另外一个奇葩操作,笔者在之前的开发中也是这样干过?,但是其实是不推荐的。
function Autowired(target, name, descriptor) {
//想给类的某个字段设置一个属性值。
target[name] = {};
}
class Person {
@Autowired
name1;
}
笔者在前文讲解babel转码class的时候说过,属性,不管是在构造器里面,还是外面,其都是定义在类的实例上的,而以上的赋值语句,我们其实是赋值在类的原形对象上的,当我们的类初始化之后,这个对象是拥有name1这个属性的,只不过是undefined罢了,而虽然我们在这个对象的原形上定义了name1,但是因为自身拥有name1属性,则自动忽略了原形对象上的name1属性。
因此,我们也可以得出一个小小的结论,想要通过属性装饰器去修改属性的值,是不可取的手段,自欺欺人而已?。
4.2 方法装饰器
相对于属性装饰,方法装饰的坑要少的多。 源码:
function log(target, name, descriptor) {
var oldValue = descriptor.value;
descriptor.value = function () {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
class Person {
@log
calc() {}
}
转码结果:
//多余的代码就不必要贴出来了,关键就是,我们的方法装饰器,不会经过_initializerDefineProperty,因此,方法就可以通过value修改,而增加一些额外的操作(比如在方法执行器做什么事,方法执行完成之后做什么事)。
var Person =
((_class = /*#__PURE__*/ (function () {
function Person() {
_classCallCheck(this, Person);
}
_createClass(Person, [
{
key: "calc",
value: function calc() {},
},
]);
return Person;
})()),
_applyDecoratedDescriptor(_class.prototype, "calc", [log], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),
_class);
4.3 多个属性或者方法叠加使用
源码:
function log(target, name, descriptor) {
var oldValue = descriptor.value;
descriptor.value = function () {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
function log2(target, name, descriptor) {
return descriptor;
}
class Person {
@log
@log2
calc() {}
}
转码结果:
//...省略代码
// 将装饰器的顺序颠倒,并且迭代执行
desc = decorators
.slice()
.reverse()
.reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
// ...省略代码
// 依次读取装饰器
_applyDecoratedDescriptor(_class.prototype, "calc", [log, log2], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),
由此可以得出结论,属性或方法装饰器跟类的装饰器的执行顺序是一致的,离的近装饰器的先执行。
4.4 带参数的属性或方法装饰器
带参数的属性或方法装饰器,和带参数的类的装饰器类似,如:
function Prop(options = {}) {
//做一些操作
return function(target, prop, descriptor){
//做一些操作
return descriptor;
}
}
此处将不再赘述。
5、总结
1、装饰器总是在类定义的时候就已经执行了;
2、类装饰器只接收一个参数,这个参数是被装饰的类本身,装饰器如果有为真的返回值,则这个值用作覆盖类,如果没有的话,就是类的本身。
3、属性或方法装饰器接收三个参数,分别是target,prop,descriptor。对于babel转码的结果,属性装饰器还有一个额外的临时属性叫作initializer,用来初始属性的值。属性装饰器可以选择返回或不返回descriptor效果是一致的,因为JS对象在传递进入函数的时候,修改之后的效果将会反应在对象上,但是建议返回descriptor。
4、永远不要尝试用属性装饰器去改写属性的值。
5、带参数的装饰器一定要写成函数的执行形式。
由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!