JavaScript Decorators 的简单理解

  Decorators,装饰器的意思, 所谓装饰就是对一个物件进行美化,让它变得更漂亮。最直观的例子就是房屋装修。你买了一套房子,可是毛坯房,你确定不想住,那就对它装饰一下,床,桌子,电视,冰箱等一通买,房子变漂亮了,住的也舒心了,同时功能也强大了,由于咱们能够看电视了,上网了。javascript

  Js中,Decorators的做用也是如此,但它做用的对象是一个类或者其属性方法,在不改变原有功能的基础上,加强其功能。语法很是简单,就是在类或者其属性方法前面加上@decorator,decorator 指的是装饰器的名称。装饰器自己是一个函数,由于在函数内部,咱们能够进行任意的操做从而对其进行加强。html

  稍微有点遗憾,Decorators并无被标准化,不过咱们有babel, 能够利用babel进行转化,就是配置有点麻烦,在学习以前,咱们先用webpack(3版本)配置一个简单的学习环境。java

装饰器的转化依赖一个核心插件 babel-plugin-transform-decorators-legacy。 新建一个decorator 文件夹,npm init -y 初始化项目,安装各类依赖 npm install webpack webpack-dev-server  babel-core  babel-loader babel-plugin-transform-decorators-legacy --save-dev, 而后新建index.js 做为入口文件,index.html用于展现,webpack.config.js  配置文件 , node

  webpack.config.js  配置文件, 在babel-loader 的options中配置了transform-decorators-legacy  插件webpack

const path = require('path');

module.exports = {
    entry: path.join(__dirname, 'index.js'),
    output: {
        path: path.join(__dirname),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: path.join(__dirname, 'node_modules'),
                options: {
                    plugins: ['transform-decorators-legacy']
                }
            }

        ]
    }
}

  由于webpack 打包后文件是bundle.js , 因此要在index.html 中引入 bundle.js , index.html 以下web

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="bundle.js"></script>
</body>
</html>

  在index.js 中先随便写点东西,验证一个配置是否正确npm

document.body.innerHTML = 'blue';

  在package.json文件中, scripts 字段中写入 "dev": "webpack-dev-server"json

  在decorator文件夹中启动命令窗口,输入npm run dev, 能够看到项目启动成功,在浏览器中输入locolhost:8080 ,能够看到blue 表示配置成功浏览器

  环境搭建好了,如今能够学习Decorators了。首先 Decorators是做用在class上面的,因此声明一个class,好比Car , babel

class Car {
   
}

   其次,Decorators是一个函数,那么咱们就写一个函数,直接命名为decorators 好了, 这个函数要有一个参数,就是要装饰的对象,名称通常命名为target, 这个也很好理解,咱们都不知道对谁进行装饰,还装饰什么。

function decorators(target) {
    target.color = 'black';
}

  咱们给target 增长一个color属性, 由此能够推断出,要装饰的类有了一个color 属性。 装饰一个类,就在类的上面写上@decorators, 咱们能够打印一下, 证实咱们的猜想是否是正确的, 整个index.js 文件以下:

// 装饰器函数
function decorators(target) {
    target.color = 'black';
}
// 用@装饰器 装饰一个类 
@decorators
class Car {
   
}
console.log(Car.color);  // 输出black

  这时你可能会想,咱们可不能够动态设置color属性的值? 固然能够,由于装饰器是一个函数,咱们只要返回这个函数就能够了,咱们来声明一个函数,让它返回装饰器函数。注意这里不能使用箭头函数。咱们把 decorators 函数作以下修改,它接受一个color 参数, 固然使用的时候也要传递一个参数

// 返回装饰器的函数
function decorators (color) {
    return function(target){
        target.color = color;
    }
}

// 使用时传递一个参数,如 'red'
@decorators('red')
class Car {
   
}

console.log(Car.color);  // 输出咱们指定的参数red.

  对于一个类的简单装饰就是这么简单。 如今咱们再来装饰一个类的方法,同时说明一下装饰器的由来。如今清空index.js,重写一下Car 类,让它有一个方法getColor

class Car {
    constructor(color) {
        this.color = color;
    }
    
    getColor() {
        return this.color;
    }
}
    

  使用这个类也很是简单,就是用new 建立一个对象,而后调用getColor 方法

let carObj = new Car('black');
console.log(carObj.getColor());  // 输出black

  可是这时不当心,从新在carObj对象身上赋值了一个getColor 方法,

carObj.getColor = function(){
    return 'blah blah';
}

  出问题了,它输出了 blah blah, 和咱们的预想不一致,问题以下

console.log(carObj.getColor()); // 输出blah blah, 咱们能够覆盖了getColor 

  在实际开发中,咱们确定不想出现这样的问题,那怎么办? 怎样才能避免这要的覆盖操做? 这时咱们想到了javascript中的一条标准,给对象进行赋值操做时,若是赋值的方法名,正好在原型链中有,也就是说与原型链中的方法重名,但原型链中该方法定义了只读属性,那么赋值操做是不容许的。咱们只要把原型链中的方法定义为只读属性就能够解决问题了,那怎样才能把原型链中的方法定义为只读属性呢? 那就是用Object.defineProperty 来定义原型链中的方法。

  这里要注意,ES6中的class语法,只是原型链方式的一种语法糖,咱们在一个class中添加方法,其实是向原型链上添加方法,也就是说getColor 方法,实际上存在于Car.prototype上, 实际上在这里,咱们也能够看看getColor的默认属性值究竟是什么样子? 当咱们在一个对象上定义方法或属性时,它都有默认的属性描述,怎么看呢? 用 Object.getOwnProtperty

console.log(Object.getOwnPropertyDescriptor(Car.prototype, 'getColor'))

   能够看到以下内容,

  它的默认属性值,writable: true, enumerable: false, configurable: true, 可写,可配置,不可枚举。这时咱们也明白了,因为writable: true  致使了它能够被复写。也就是说,若是咱们在类中写方法,是没有办法阻止它被复写的,因此咱们要用object.defineProperty 在类的外面添加方法,对它进行配置。 把getColor 从类中删除,object.defineProperty 从新定义。整个js代码以下:

class Car {
    constructor(color) {
        this.color = color;
    }
}

// 用Object.defineProperty 在原型链上定义方法,从而能够进行属性配置
// value 的值也能够是一个函数,之前一直觉得它只能是数值
Object.defineProperty(Car.prototype, 'getColor', {
    value:function () {
        return this.color;
    },
    writable: false
})
    
let carObj = new Car('black');
console.log(carObj.getColor());  // 输出black

carObj.getColor = function(){
    return 'blah blah';
}
console.log(carObj.getColor()); // 输出black

   当咱们进行配置之后,纵然能够添加同名属性,但不会被复写了。但这又有了一个问题,若是多个属性都要求不可复写时,都要按照上面的方法进行配置,那就太麻烦了,因此咱们要写一个函数,对代码进行封装。由于咱们这里只是改了descriptor ,因此咱们能够把它提出来,声明成一个变量, 而后利用函数对其进行修改。 descriptor 的初始值是什么呢?上面咱们说过,系统会为每个属性设一个默认值,咱们使用这个默认值确定不会报错

// 当咱们在类中写一个方法时,默认的属性描述就是下面
let descriptor = {
    value: function() {
        return this.color;
    },
    writable: true,
    configurable: true,
    enumerable: false
}

  而后再写一个函数,命名为readonly吧,由于不可复写吗, 在里面修改descriptor, 并返回。 为了更为准确的说明,咱们仍是写上target, key,来表示咱们修改哪一个对象的哪一个属性

let readonly = function(target, key, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

  再调用 readonly 来修改咱们的descriptor,  最后object.defineProperty 从新定义

descriptor =  readonly(Car.prototype, 'getColor', descriptor);

Object.defineProperty(Car.prototype, 'getColor', descriptor)

  这时咱们的要求要达到了,整个js 代码以下

class Car {
    constructor(color) {
        this.color = color;
    }
}
// 当咱们在类中写一个方法时,默认的属性描述就是下面
let descriptor = {
    value: function() {
        return this.color;
    },
    writable: true,
    configurable: true,
    enumerable: false
}
let readonly = function(target, key, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

descriptor =  readonly(Car.prototype, 'getColor', descriptor);

Object.defineProperty(Car.prototype, 'getColor', descriptor)
    
let carObj = new Car('black');
console.log(carObj.getColor());  // 输出black

carObj.getColor = function(){
    return 'blah blah';
}
console.log(carObj.getColor()); // 输出black

  咱们再往下一步,只把readonly 函数留下,而且放到js 代码的顶部,同时再把getColor 函数放到class类中, js 代码以下

// readonly函数
let readonly = function(target, key, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

class Car {
    constructor(color) {
        this.color = color;
    }
    getColor() {  // getColor 从新写到类中
        return this.color;
    }
}

let carObj = new Car('black');
console.log(carObj.getColor());  // 输出black

carObj.getColor = function(){
    return 'blah blah';
}
console.log(carObj.getColor()); // 输出blah blah

  你可能好奇readonly函数怎么用? 其实它就是咱们的装饰器函数, 只要把@readonly 放到getColor 的上面, 咱们相要的效果也能达到

class Car {
    constructor(color) {
        this.color = color;
    }
    @readonly  // 加上readonly
    getColor() { 
        return this.color;
    }
}

  这时你可能明白了,装饰器实际上是利用object.defineProperty 从新定义了属性或方法。

  正着推理已经完成了,咱们再反着试一试, js代码改成以下样式

let readonly = function(target, key, descriptor) {
    console.log(target);
    console.log(key);
    console.log(descriptor);
}

class Car {
    constructor(color) {
        this.color = color;
    }
    @readonly 
    getColor() { 
        return this.color;
    }
}

  咱们依次输出了装饰器的target, key, descripter 三个参数,target 就是Car.prototype, key 就是指getColor 自己, descriptor 就是咱们的属性描述符

  也就是说,当咱们把一个装饰器函数写到一个方法或类上时,js 引擎会自动的把target,key, descriptor 注入到装饰器函器中,以便咱们修改,从而从新定义函数,这给咱们动态地修改提供了可能。

相关文章
相关标签/搜索