装饰器
装饰器又叫修饰器(Decorators),是一种特殊类型的声明,它可以附加到类声明、方法、参数或者属性上。装饰器由@符号紧接一个函数名称,形如@expression,expression求值后必须是一个函数,在函数执行的时候装饰器的声明方法会被立即执行。装饰器是用来给附着的主体进行装饰,添加额外行为的一种方式。
许多面向对象的语言都有装饰器函数。
ES6中也引入了关于装饰器的一个提案,但是目前大多数浏览器中以及node环境中都不支持装饰器修饰,若是需要学习装饰器的功能,需要先将装饰器的代码转换成浏览器或者node能够识别的低版本的js代码,这就需要用到转换js代码的babel工具。为了学习装饰器的效果,需要在本地安装好环境,做如下操作:
1.确保本机中安装node和npm环境
例如我的环境下node版本是v6.9.5,npm版本是3.10.10
2.在本地任何文件夹下创建learn-decorator文件夹,执行npm init生成package.json文件。
3.安装babel
npm install --save-dev babel-cli babel-preset-env
在本地安装上述两个库,在工程根目录上创建babel的配置文件.babelrc,并写入:
{
"presets": ["env"]
}
4.安装decorators插件
如果不安装插件,会有语法错误提示。按照说明,继续安装插件。
npm install babel-plugin-transform-decorators-legacy --save-dev
之后再配置文件添加插件,现在.babelrc文件应如下所示:
{
"presets": ["env"],
"plugins": ["transform-decorators-legacy"]
}
这样准备工作就已经完成了。用vscode打开工程项目,创建一个main.js文件,打开终端,可以在终端中按照如下方式
使用babel命令(安装babel-cli时,会安装babel和babel-node命令):windows中
.\node_modules\.bin\babel-node main.js // 直接运行js代码
.\node_modules\.bin\babel main.js > test.js // 将main.js转换成
关于babel中为什么要配置成babel-preset-env方式,是为了以兼容的形式把babel-preset-env嵌入到babel里面,
babel preset将基于你的实际浏览器及运行环境,自动的确定babel插件及polyfills,转译ES2015及此版本以上的语言。这里不再具体叙述babel的配置,
详情请参考[babel配置](https://segmentfault.com/a/1190000011639765)。
1. 类的装饰器
显然,类的装饰器是修饰类的,可以修饰类本身,比如添加静态变量,也可以修饰类实例,比如添加一个属性。
1.1 修饰类本身
@testDecorator
class ClassA {
}
function testDecorator(target) {
target.addedParam = "I am decorator";
}
console.log(ClassA.addedParam);
// 在终端中执行.\node_modules\.bin\babel-node main.js 输出 I am decorator
上述代码中testDecorator就是一个修饰器。它修改了ClassA这个类的行为,为这个类添加了一个静态的属性addedParam。testDecorator函数的参数是ClassA类本身。
可以将上述操作理解如下:
@testDecorator
class ClassA {}
// 等同于
class ClassA {}
ClassA = testDecorator(ClassA) || ClassA;
也就是说修饰器是一个对类进行处理的函数,修饰器的第一个参数就是所要修饰的目标,在本例中就是ClassA。
修饰的目标是默认传入的参数,还可以传入其他的参数。
function testDecorator(desc) {
return function(target) {
target.addedParam = desc;
}
}
//调用testDecorator函数之后,其又返回一个匿名函数,利用闭包的特性使用了decs变量
@testDecorator("operate one")
class ClassA {
}
console.log(ClassA.addedParam) // operate one
@testDecorator("operate two")
class ClassB {
}
console.log(ClassB.addedParam) // operate two
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着修饰器能在编译阶段运行代码,也就是说修饰器本质就是编译时执行的函数。
通过将main.js转换为node支持的格式看一下:运行.\node_modules.bin\babel main.js > test.js命令
"use strict";
var _dec, _class;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function testDecorator(desc) {
return function (target) {
target.addedParam = desc;
};
}
var ClassA = (_dec = testDecorator("operate one"), _dec(_class = function ClassA() {
_classCallCheck(this, ClassA);
}) || _class);
console.log(ClassA.addedParam);
若是想添加实例属性,可以通过目标类的prototype对象操作。
function paramDecorator(target) {
target.prototype.isTestable = "yes";
}
@paramDecorator
class ClassB{
}
let B = new ClassB();
console.log(B.isTestable); // 输出 yes
又如下面的例子,通过msxins装饰器给类添加上新属性。
function maxins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
}
}
let Foo = {
foo: () => {
console.log('foo');
}
}
Foo.foo();
@maxins(Foo)
class ClassC {
}
let C = new ClassC();
C.foo();
console.log(C)
=========================
foo
foo
ClassC {} // 可见ClassC本身是没有foo属性的
1.2 修饰类的方法
class Person{
@readonly
name() {
console.log('is readonly')
}
}
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
console.log('name', name)
descriptor.writable = false;
return descriptor;
}
let person = new Person();
person.name();
============================
name name
is readonly
实现输出日志的操作:
class Math {
@log
add(a, b) {
return a + b;
}
}
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;
}
const math = new Math();
// passed parameters should get logged now
math.add(2, 4);
================================
Calling add with { '0': 2, '1': 4 }
装饰器还具有注释的功能,根据装饰器的名称,能够看出类或者方法有哪些功能。如果同一个方法有多个装饰器,则会向剥洋葱一样,从外到内进入,然后从内到外依次执行。
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id); // 必须要定义的回调
函数,否则运行会报错。
}
class Example {
@dec(1)
@dec(2)
method(){
console.log('method');
}
}
let example = new Example();
example.method();
=================================
evaluated 1
evaluated 2
executed 2
executed 1
method
除了注释功能,装饰器还可以用来类型检查,因此这一功能相当重要。
1.3 为什么不能将装饰器用于函数?
装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
上面的代码,意图是执行后counter等于 1,但是实际上结果是counter等于 0。因为函数提升,使得实际执行的代码是下面这样。
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
counter++;
};
总之,由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
如果一定要修饰函数,可以采用高阶函数的形式直接执行。
“`
function doSomething(name) {
console.log(‘Hello, ’ + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log(‘Starting’);
const result = wrapped.apply(this, arguments);
console.log(‘Finished’);
return result;
}
}
const wrapped = loggingDecorator(doSomething);