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

  Js中,Decorators的作用也是如此,但它作用的对象是一个类或者其属性方法,在不改变原有功能的基础上,增强其功能。语法非常简单,就是在类或者其属性方法前面加上@decorator,decorator 指的是装饰器的名称。装饰器本身是一个函数,因为在函数内部,我们可以进行任意的操作从而对其进行增强。

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

装饰器的转化依赖一个核心插件 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  配置文件 ,

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

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 如下

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

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

document.body.innerHTML = 'blue';

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

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

  环境搭建好了,现在可以学习Decorators了。首先 Decorators是作用在class上面的,所以声明一个class,比如Car ,

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 注入到装饰器函器中,以便我们修改,从而重新定义函数,这给我们动态地修改提供了可能。

JavaScript Decorators 的简单理解的更多相关文章

  1. javascript --- 递归的简单理解

    递归函数大家都应该比较熟吧?那么,如何在JavaScript中书写一个完美的递归函数呢?且听我娓娓道来. 递归函数 写的时候,查了一下维基百科对递归函数的定义,恕我愚钝,简直太深奥了!所以,我还是简单 ...

  2. javascript 闭包最简单理解

    首先说3点与闭包有关系的东西. 一.变量的作用域 变量的作用域不难理解. 1.函数内部可以访问函数外部的变量,而函数外部不能访问函数内部的变量. 2.如果在函数内定义变量的时候,不加var,那么是全局 ...

  3. JavaScript设计模式的简单理解

    设计模式可以理解为一系列的代码框架,我觉得主要涉及封装的概念.把实现某一功能的代码段封装在函数中,可以方便调用,同时利于代码的复用,提高了代码的可维护性.下面简单介绍一下几种设计模式的个人感受. 1. ...

  4. javascript笔记—— call 简单理解

    call 方法 请参阅 应用于:Function 对象 要求 版本 5.5 调用一个对象的一个方法,以另一个对象替换当前对象. call([thisObj[,arg1[, arg2[, [,.argN ...

  5. 简单理解JavaScript闭包

    很多关于JS的书籍例如<JavaScript权威指南>或者<高程>都把闭包解释的晦涩难懂,萌新们是怎么也看不懂啊!不过别怕,今天我就用很简单的方式给大家讲解下到底什么是闭包.这 ...

  6. javascript编写一个简单的编译器(理解抽象语法树AST)

    javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...

  7. Javascript的堆和栈的简单理解

    <!doctype html> <html> <head> <meta charset="UTF-8"> <title> ...

  8. 简单理解JavaScript原型链

    简单理解原型链 什么是原型 ? 我是这样理解的:每一个JavaScript对象在创建的时候就会与之关联另外一个特殊的对象,这个对象就是我们常说的原型对象,每一个对象都会从原型"继承" ...

  9. Javascript闭包简单理解

    提到闭包,想必大家都早有耳闻,下面说下我的简单理解.平时写代码.第三方框架和组件都或多或少用到了闭包.所以,了解闭包是非常必要的.呵呵... 一.什么是闭包简而言之,就是能够读取其他函数内部变量的函数 ...

随机推荐

  1. WPF保存包含Winform控件的XAML页面问题

    原文:WPF保存包含Winform控件的XAML页面问题 最近的工作中,用到了WPF调用Winform控件 但是在保存XAML页面的时候发现了问题,就是Winform页面黑黑的,没有任何渲染的波形曲线 ...

  2. mybatis百科-结果集映射类ResultMap

    目录 1 成员变量 2 构造函数 3 其他函数 3.1 setter 和 getter 函数 4 静态内部类 4.1 成员变量 4.2 构造函数 4.3 建造者相关的函数 4.4 获取配置的构造方法参 ...

  3. RabbitMQ 3.6.1 升级至 3.7.9 版本(Windows 升级至Centos)

    随着公司业务量的增加,原本部署在Windows服务器的RabbitMQ集群(3.6.1)总是出现莫名其妙的问题,经查询官方Issue,确认是RabbitMQ 3.6.1 版本的bug.查看从3.6.1 ...

  4. .NET Core 2.1中的分层编译(预览)

    如果您是.NET性能的粉丝,最近有很多好消息,例如.NET Core 2.1中的性能改进和宣布.NET Core 2.1,但我们还有更多的好消息.分层编译是一项重要的新特性功能,我们可以作为预览供任何 ...

  5. 避免使用HttpClient的系统代理

    这两天在玩Consul, 他的.Net驱动使用了HttpClient来发送Http请求. 但是我的电脑上装有SS, 所以请求会被SS过滤一次, 然后导致请求的延迟一直比较高. 然后只需要改写一下Htt ...

  6. Atcoder F - LCS (DP-最长公共子序列,输出字符串)

    F - LCS Time Limit: 2 sec / Memory Limit: 1024 MB Score : 100100 points Problem Statement You are gi ...

  7. Day2 Python基础之基本操作(一)

    1.常用命令 调用cmd窗口 Win+R cmd命令窗口清屏 cls cmd命令窗口在运行python时清屏 import os i=os.system('cls') cmd命令窗口在运行python ...

  8. iOS开发造轮子 | 通用占位图

    https://www.jianshu.com/p/beca3ac24031 实际运用场景: 没网时的提示view,tableView或collectionView没内容时的展示view,以及其它特殊 ...

  9. Python_内置函数之map()

    map 会根据提供的函数对指定序列做映射. 代码如下: def square(x): return x ** 2 ret = map(square, [1, 2, 3, 4, 5]) # 计算列表各元 ...

  10. js总结:对于字符串的切割截取和合并

    1.函数:split() 功能:使用一个指定的分隔符把一个字符串分割存储到数组 例子: str=”jpg|bmp|gif|ico|png”; arr=str.split(”|”); //arr是一个包 ...