不久前,我开发了一个react
应用,使用mobx
作状态管理。这是一个时而兴奋时而困惑,但整体而言很享受的经历,很快我将会把它写出来。在使用mobx
开发时,我发现了一个很是有趣的独特之处,那就是它使用装饰器来注释类的属性。我以前在写javascript
时还没用过它,但自从我使用了mobx
提供的这个功能以及作了一些开发后,我发现这是一个有巨大潜力的功能。javascript
装饰器如今还不是javascript
的核心特性,他们正经过ECMATC39的标准化流程进行工做。不过并不表明咱们不能去熟悉它。
在不久的未来,它将获得浏览器和node
的原生支持,与此同时,babel
也获得支持。html
Decorator
是decorator function/methored
的缩写。它是一个函数,它会经过返回一个新函数来修改传入的函数或方法的行为。java
你能够在函数式编程的任何语言中实现装饰器,好比javascript
,你能够把函数绑定到一个变量上,也能够把函数当成函数的参数传递。这些语言中的几种有特殊的语法糖,用来定义和使用装饰器,其中一个就是python
:node
def cashify(fn): def wrap(): print("$$$$") fn() print("$$$$") return wrap @cashify def sayHello(): print("hello!") sayHello() # $$$$ # hello! # $$$$
让咱们看看发生了什么,cashify
函数是一个装饰器,他接受一个函数做为参数,它的返回值也是函数。咱们使用python
的pie syntax
把装饰器应用到sayHello
函数上,本质上和咱们在sayHello
的定义下执行此操做是同样的:python
def sayHello(): print("hello!") sayHello = cashify(sayHello)
不管咱们装饰的函数打印什么,最后的结果都会在他们先后打印$符号。react
为何我要使用python
的例子来介绍ECMAScript
的装饰器,很高兴你问这个问题!git
python
是一个很好地方式去解释基础知识,由于它的装饰器的概念比它在JS中的工做方式更简单直接js
和TS
都是用python
的pie syntax
把装饰器应用到类的函数和属性上,因此它们外观和语法格式都很类似好了,那么js
装饰器有什么不一样呢?es6
python
把传入的须要装饰的任何函数当作参数,但由于对象在js
中的特殊工做方式,js装饰器能够获取到更多信息。github
对象在js
中有属性,而且这些属性有如下值:编程
const oatmeal = { viscosity: 20, flavor: 'Brown Sugar Cinnamon', };
但除了它的值,每一个属性还有一些其余隐藏的信息,用于定义它工做方式的不一样方面,叫作属性描述符:
console.log(Object.getOwnPropertyDescriptor(oatmeal, 'viscosity')); /* { configurable: true, enumerable: true, value: 20, writable: true } */
JS在追踪与这个属性有关的不少东西:
configurable
决定该属性的类型可否被修改,以及它可否从对象中删除enumerable
控制当你在枚举对象属性时,该属性是否显示(好比当你调用Object.keys(oatmeal)
或者使用for
循环时)writable
控制你是否能够经过赋值操做符=
修改该属性的值value
是你访问这个属性时,所看到的静态值。一般,这是你常常看到和关心的属性描述符的惟一部分。它能够是任何JS
值,包括一个函数,这会使这个属性成为其所属对象的方法。属性描述符也有两个其余的属性,为访问器描述符(一般称为getter
和setter
):
get
是一个返回属性值而不是用静态value
属性的的函数set
是一个特殊的函数,当你给这个属性赋值时,该函数会将你在等号右边放置的任何内容做为参数js
从es5
就已经有了操做属性描述符的API
,经过Object.getOwnPropertyDescriptor
和Object.defineProperty
的形式。好比我喜欢个人燕麦片的浓度,我可使用这个API
像下边这样把它变成只读的:
Object.defineProperty(oatmeal, 'viscosity', { writable: false, value: 20, }); // 当我试图设置oatmeal.viscosity为不一样的值时,它将会默默地报错 oatmeal.viscosity = 30; console.log(oatmeal.viscosity); // => 20
我甚至能够写一个通用的decorate
函数,能够修改任何对象的任何属性的修饰符
function decorate(obj, property, callback) { var descriptor = Object.getOwnPropertyDescriptor(obj, property); Object.defineProperty(obj, property, callback(descriptor)); } decorate(oatmeal, 'viscosity', function(desc) { desc.configurable = false; desc.writable = false; desc.value = 20; return desc; });
第一个主要的装饰器的提案只与ES
的类有关,而非普通对象。让咱们设计一些类来表明咱们的粥:
class Porridge { constructor(viscosity = 10) { this.viscosity = viscosity; } stir() { if (this.viscosity > 15) { console.log('This is pretty thick stuff.'); } else { console.log('Spoon goes round and round.'); } } } class Oatmeal extends Porridge { viscosity = 20; constructor(flavor) { super(); this.flavor = flavor; } }
咱们使用一个类来表明咱们的燕麦粥,他继承自一个更通用的的 Porridge
类。Oatmeal
设置了默认的浓度来覆盖Porridge
的默认值,而且添加了新的口味属性。咱们也使用了另外一个es
提案 class fields去覆盖浓度属性。
咱们能够从新建立咱们原始的燕麦粥了:
const oatmeal = new Oatmeal('Brown Sugar Cinnamon'); /* Oatmeal { flavor: 'Brown Sugar Cinnamon', viscosity: 20 } */
很好,咱们获得了咱们的es6
燕麦粥,咱们要准备写装饰器了!
js
装饰器函数被传入三个参数:
target
是咱们对象所继承的类key
是咱们应用装饰器的属性的名称,为字符串。descriptor
是属性描述符对象咱们在装饰器内作什么依赖于咱们装饰器的目的。为了装饰对象的方法和属性,咱们须要返回一个新的属性描述器。咱们能够经过如下方式写一个装饰器来使一个属性为只读:
function readOnly(target, key, descriptor) { return { ...descriptor, writable: false, }; }
咱们能够像这样修改咱们的oatmeal类:
class Oatmeal extends Porridge { @readOnly viscosity = 20; // 你也能够吧@readonly放在属性上一行 constructor(flavor) { super(); this.flavor = flavor; } }
如今咱们燕麦粥像胶水同样的浓度不会被干预了,谢天谢地。
若是咱们想作一些真正有用的东西呢?我在最近的项目时遇到了一种状况,其中装饰器节省了我不少开发和维护的开销。
在我开头提到的Mobx/React app
中,我有一些不一样的类做为数据中心。他们各自都表明与用户交互的不一样类别的集合,而且与不一样的API
端点对话以获取服务端的数据。为了处理API
错误,我使每一个数据中心在与网络通讯时都准守一个协议:
ui
中心的networkStatus
属性为loading
api
请求处理结果
ui
中心的apiError
属性为接收到的错误ui
中心的networkStatus
属性为idle
我发如今我注意到以前,已经重复了不少次这种模式:
class WidgetStore { async getWidget(id) { this.setNetworkStatus('loading'); try { const { widget } = await api.getWidget(id); // Do something with the response to update local state: this.addWidget(widget); } catch (err) { this.setApiError(err); } finally { this.setNetworkStatus('idle'); } } }
这是不少错误处理的样板。由于我已经在全部更新可观察属性的方法上使用了MobX
的@action
装饰器了(为了简单起见,此处未显示),因此也能够再添加一个装饰器用来节省我错误处理的代码。我想出了这个:
function apiRequest(target, key, descriptor) { const apiAction = async function(...args) { // More about this line shortly: const original = descriptor.value || descriptor.initializer.call(this); this.setNetworkStatus('loading'); try { const result = await original(...args); return result; } catch (e) { this.setApiError(e); } finally { this.setNetworkStatus('idle'); } }; return { ...descriptor, value: apiAction, initializer: undefined, }; }
而后我就能够像这样替换那些写在每一个API
操做方法上的模板:
class WidgetStore { @apiRequest async getWidget(id) { const { widget } = await api.getWidget(id); this.addWidget(widget); return widget; } }
个人错误处理代码依然在那,可是我只须要写一次,而且确保每一个使用它的class
都有setNetworkStatus
和setApiError
方法便可。
我选择descriptor.value
和调用descriptor.initializer
其中之一的那一行发生了什么?这是与babel相关的事。个人预感是,这种方式在js
原生支持装饰器的时候不会起做用,但当考虑到babel处理做为类属性的箭头函数的方式时,就会颇有必要。
当你定义一个类属性,而且给它赋值一个箭头函数时,babel
会巧妙地把函数绑定到类正确的实例上而且提供你正确的this
值。经过设置descriptor.initializer
为一个函数,它会返回你写的那个函数,而且在其做用域内为正确的this
值。
一个例子会让事情变简单:
class Example { @myDecorator someMethod() { // 在这个例子中,咱们的方法能够由descriptor.value引用到 } @myDecorator boundMethod = () => { // 在这里,descriptor.initializer是一个函数,他会返回咱们的boundMethod函数,而且this执行已经被调整为Example的实例 }; }
除了属性和方法,你还能够装饰整个类。想要装饰类,你只须要传入装饰器函数的第一个参数target
。好比,我想写一个自动把类注册为自定义html
标签的装饰器,我在这里使用了一个闭包,来保证装饰器可以接收咱们想要为标签提供参数的任何名称:
function customElement(name) { return function(target) { // customElements是一个全局API,用来建立自定义标签 customElements.define(name, target); }; }
咱们将这样使用它:
@customElement('intro-message'); class IntroMessage extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); this.wrapper = this.createElement('div', 'intro-message'); this.header = this.createElement('h1', 'intro-message__title'); this.content = this.createElement('div', 'intro-message__text'); this.header.textContent = this.getAttribute('header'); this.content.innerHTML = this.innerHTML; shadow.appendChild(this.wrapper); this.wrapper.appendChild(this.header); this.wrapper.appendChild(this.content); } createElement(tag, className) { const elem = document.createElement(tag); elem.classList.add(className); return elem; } }
把它加入到咱们的html中,能够这样使用它:
<intro-message header="Welcome to Decorators"> <p>Something something content...</p> </intro-message>
浏览器中显示以下:
现在在你的项目中使用装饰器须要一些转译配置。我所见的最直接的教程就在MobX
的文档中,它有TS
和两个主要版本的babel
信息。
请记住装饰器当前仍是发展中的提议,若是你在生产代码中使用它,你可能须要作一些更新或者持续使用babel
装饰器插件,直到它成为ECMA
官方的正式规范。甚至babel也没有很好地支持,最新版的装饰器提案包含很大的改动,并无很好地向后兼容上一个版本。
装饰器像不少最新的js特性同样,是你工具箱中颇有用的工具,他很大程度的简化了不一样和不相关的类的行为共享。然而过早的采用总须要一些成本。因此使用装饰器,也须要了解它对你代码库的影响。