當你們都再聊要不要學習框架的時候,筆者卻還在學規範,當標題黨。本文的一切,源於網絡,感恩開源的世界...javascript
雖然本文的初衷是講 ES7 中的裝飾器,但筆者更喜歡在探索的過程當中加深對前端基礎知識的理解。本着一顆刨根問底兒的心,分享內容會盡量多地將一些關聯知識串聯起來說解。html
乍一看可能會有點亂,但倒是筆者學習一個新知識的完整路徑。 一種帶着關鍵詞去學習的方法,比較笨,讀者選讀便可,取精華去糟粕。前端
另外,這個倉庫 是專門用來記錄 Decorators 低侵入性探索 收穫的知識。後續可能會結合 mobx 源碼、以及在 React 中實際應用場景來深刻。vue
前端知識廣度一望無際,深度深不可測,筆者記性很差,相似的倉庫有:java
Decorators 屬於 ES7, 目前處於提案階段,可經過 babel
或 TS
編譯使用。git
本文屬於探索型,主要分爲三部分:github
Decorators 基礎知識npm
Babel 與 TypeScript 支持json
常見應用場景數組
裝飾器 (Decorators) 讓你能夠在設計時對類和類的屬性進行「註解」和修改。
Decorators
通常接受三個參數:
目標對象 target
屬性名稱 key
描述對象 descriptor
可選地返回一個描述對象來安裝到目標對象上,其的函數簽名爲
function(target, key?, descriptor?)
。
Decorators
的本質是利用了 ES5 的 Object.defineProperty
方法,這個方法着實改變了不少,好比 vue 響應式數據的實現方法,固然還有更爲迷人 proxy
,是否是發現,不少框架背後的靠山都離不開這些底層規範的支持。
下面來簡單瞭解下這個方法:
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
Object.defineProperty(obj, prop, descriptor)
obj
要在其上定義屬性的對象。
prop
要定義或修改的屬性的名稱。
descriptor
將被定義或修改的屬性描述符。
返回值
被傳遞給函數的對象。
其中 descriptor
可經過 Object.getOwnPropertyDescriptor()
方法得到。
Object.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor()
方法返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不須要從原型鏈上進行查找的屬性)
obj
須要查找的目標對象
prop
目標對象內屬性名稱(String 類型)
返回值
若是指定的屬性存在於對象上,則返回其屬性描述符對象(property descriptor),不然返回 undefined
。
一個屬性描述符是一個記錄,由下面屬性當中的某些組成的:
value
該屬性的值(僅針對數據屬性描述符有效)
writable
當且僅當屬性的值能夠被改變時爲 true
。(僅針對數據屬性描述有效)
configurable
當且僅當指定對象的屬性描述能夠被改變或者屬性可被刪除時,爲 true
。
enumerable
當且僅當指定對象的屬性能夠被枚舉出時,爲 true
。
get
獲取該屬性的訪問器函數(getter
)。若是沒有訪問器, 該值爲 undefined
。(僅針對包含訪問器或設置器的屬性描述有效)
set
獲取該屬性的設置器函數(setter
)。 若是沒有設置器, 該值爲 undefined
。(僅針對包含訪問器或設置器的屬性描述有效)
各式的裝飾器通常都是基於修改上述屬性來實現,好比 writable
可用於設置 @readonly
。更多的功能,可參考 lodash-decorator
如今咱們對 Decorators 方法 function(target, key?, descriptor?)
混了個臉熟,同時知道了Object.defineProperty
和 Descriptor
與 Decorators 的聯繫。
可是,目前瀏覽器對 Es7 這一特性支持 並不友好。Decorators 目前還只是語法糖,嚐鮮可經過 babel 、TypeScript。
接下來就來了解這一部分的內容。
不少構建工具都離不開 babel,好比筆者用於快速跑 demo 的 parcel。雖然不少時候咱們並不須要關心這些構建後的代碼,但筆者建議有時間仍是多瞭解下,畢竟前端打包後出現的 bug 仍是很常見的。
回到裝飾器,現階段官方說有 2 種裝飾器,但從實際使用上可分爲 4 種,分別是:
「類裝飾器」 做用於 class
。
「屬性裝飾器」 做用於屬性上的,這須要配合另外一個的類屬性語法提案,或者做用於對象字面量。
「方法裝飾器」 做用於方法上。
「訪問器裝飾器」 做用於 getter
或 setter
上的。
下面咱們經過 babel 命令行,來感覺一下各裝飾器:
先簡單介紹下 babel 的用法:
babel
npm i -g babel
複製代碼
.babelrc
{
"presets": [["es2015", { "modules": false }]],
"plugins": ["transform-decorators-legacy", "transform-class-properties"],
"env": {
"development": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
}
複製代碼
package.json
配置 npm script{
"babel": "babel ./demo/demo.js -w --out-dir dist"
}
複製代碼
該命令的意思是:監聽 demo
目錄下 demo.js
文件,並將編譯結果輸出到 dist
目錄
下面列出各裝飾器在 babel
編譯後對應的輸出結果。
從編譯後的結果能夠看到,autobind
做爲裝飾器只接受了一個參數,也就是類自己(構造函數)。
class MyClass = {}
MyClass = autobind(MyClass) || MyClass
複製代碼
bebel 對於方法裝飾器
的處理會比較特別,下面看下核心處理:
var _class;
// 一、首先,初始化一個 class
var initClass = (_class = (function() {
// ... 類定義
})());
// 二、經過 `_applyDecoratedDescriptor` 方法使用傳入的裝飾器對 `_class.prototype` 中的方法進行裝飾處理。
var Decorator = _applyDecoratedDescriptor(
_class.prototype,
'getName',
[autobind],
Object.getOwnPropertyDescriptor(_class.prototype, 'getName'),
_class.prototype
);
// 三、利用逗號操做符的做用,返回裝飾完的 `_class`
var MyClass = (initClass, Decorator, _class);
複製代碼
後續會對 _applyDecoratedDescriptor
方法進一步講解。
逗號操做符 對它的每一個操做數求值(從左到右),並返回最後一個操做數的值。
「訪問器裝飾器」 的處理方式與 「方法裝飾器」相似。
區別在於傳入的第三個參數 Descriptor
並非由 Object.getOwnPropertyDescriptor(_class.prototype, 'getName')
返回的,而且多了一個 Descriptor
上並不存在的 initializer
屬性供 _applyDecoratedDescriptor
方法使用。
_applyDecoratedDescriptor(
_class.prototype,
'getName',
[autobind],
// Object.getOwnPropertyDescriptor(_class.prototype, 'getName'),
{
enumerable: true,
initializer: function initializer() {
return function() {};
}
}
))
複製代碼
接下來就讓咱們來看一下 _applyDecoratedDescriptor
都作了哪些事
_applyDecoratedDescriptor
_applyDecoratedDescriptor
實際上是對 decorator
的一個封裝,用於處理多種狀況。其接受的參數跟 decorator
大致一致。
target
目標對象
property
屬性名稱
descriptor
屬性描述對象
decorators
裝飾器函數 (數組,表示可傳入多個裝飾器)
context
上下文
返回值
屬性描述對象
function _applyDecoratedDescriptor( target, property, decorators, descriptor, context ) {
// 一、經過傳入參數 `descriptor` 初始化最終導出的 `屬性描述對象`
var desc = {};
Object['ke' + 'ys'](descriptor).forEach(function(key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
// 二、存在 `value` 或者 class 初始化屬性 則將 `writable` 設置爲 `true`
if ('value' in desc || desc.initializer) {
desc.writable = true;
}
// 三、處理傳入的 decorator 函數
// 其中 `reverse` 保證了,當同一個方法有多個裝飾器,會由內向外執行。
desc = decorators
.slice()
.reverse()
.reduce(function(desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
// 看 babel 編譯後的代碼,當 `initializer` 不爲 `undefined` 時,並不會傳入 `context`
// 筆者看不懂! ??? 這是一個永遠不會執行的邏輯... 難道改走 `_initDefineProp` 邏輯了?
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
// 4. 使用 Object.defineProperty 對 `target` 對象的 `property` 屬性賦值爲 `desc`
if (desc.initializer === void 0) {
Object['define' + 'Property'](target, property, desc);
desc = null;
}
return desc;
}
複製代碼
void 運算符 對給定的表達式進行求值,而後返回
undefined
。
如今咱們對 Descorators
有了大體的瞭解,接下來看下 Descorators 基於 babel 編譯下的裝飾器
this
咱們先來看一個關於 this
的問題
this
的指向問題class Person {
getPerson() {
return this;
}
}
let person = new Person();
const { getPerson } = person;
getPerson() === person; // false
person.getPerson() === person; // true
複製代碼
這段代碼中,getPerson
和 person.getPerson
指向同一個函數且返回 this
,但它們的執行結果卻不同。
this
指的是函數運行時所在的環境:
getPerson()
運行在全局環境,因此 this
指向全局環境
person.getPerson
運行在 person
環境,因此 this
指向 person
關於 this
的原理能夠參考 這篇:
在本例中,getPerson()
是一個函數,JavaScript 引擎會將函數單獨保存在內存中,而後再將函數的地址賦值給 getPerson
屬性的 value
屬性 (descriptor)
因爲函數單獨存在於內存中,因此它能夠在不一樣的環境 (上下文) 執行。
來看個例子:
// 注意,這裏都是用 var 聲明變量
var name = 'globalName';
var fn = function() {
console.log(this.name);
return this.name;
};
var person = {
getPerson: fn,
name: 'personName'
};
// 單獨執行
var ref = person.getPerson;
ref();
// or
fn();
// person 環境指執行
person.getPerson();
複製代碼
函數能夠在不一樣的運行環境 (context),因此須要一種機制,可以在函數體內部得到當前的運行環境。
這裏 this
的設計目的就是在函數體內部,指代函數當前的運行環境。
例子中,fn()
和 ref()
的運行環境都是 全局運行環境 而 person.getPerson()
的運行環境是 person
,所以獲得了不一樣的 this
解決 this
指向的方法有不少種,好比函數的原型方法
經過上面學習到的知識,接着來說解 Decorator
中如何實現 autobind
給函數或類自動綁定 this
1、 首先來看下 如何給類的方法自動綁定 this
:
var obj = {
fn: function() {
console.log('執行時的', this);
}
};
var fn = Object.getOwnPropertyDescriptor(obj, 'fn').value;
Object.defineProperty(obj, 'fn', {
get() {
console.log('get 訪問器裏的', this);
return fn;
}
});
var fn = obj.fn;
fn();
obj.fn();
複製代碼
能夠獲得的一個結論:get(){}
訪問器屬性裏面的 this
始終指向 obj
這個對象。
若是簡化邏輯,也就是不考慮其餘特殊狀況下,autobindMethod
應該是這樣的:
function autobindMethod(target, key, { value: fn, configurable, enumerable }) {
return {
configurable,
enumerable,
get() {
const boundFn = fn.bind(this);
defineProperty(this, key, {
configurable: true,
writable: true,
enumerable: false,
value: boundFn
});
return boundFn;
},
set: createDefaultSetter(key)
};
}
複製代碼
bind() 方法建立一個新的函數, 當這個新函數被調用時 this 鍵值爲其提供的值,其參數列表前幾項值爲建立時指定的參數序列。
有了 autobind
這個裝飾器,getName
方法的 this
就始終指向實例對象自己了。
class TestGet {
@autobind
getName() {
console.log(this);
}
}
複製代碼
2、接着來看下類的 autobind
實現
對類綁定 this
其實就是爲了批量給類的實例方法綁定 this
因此只要獲取全部實例方法,再調用 autobindMethod
便可。
function autobindClass(klass) {
const descs = getOwnPropertyDescriptors(klass.prototype);
const keys = getOwnKeys(descs);
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
const desc = descs[key];
if (typeof desc.value !== 'function' || key === 'constructor') {
continue;
}
defineProperty(
klass.prototype,
key,
autobindMethod(klass.prototype, key, desc)
);
}
}
複製代碼
以上實現考慮的是 Babel 編譯後的文件,除了 Babel ,TypeScript 也支持編譯 Decorators。
所以就須要一個更爲通用的 Decorators 包裝函數,接下來讓咱們一塊兒實現它。
先來一塊兒看下 TypeScript 編譯後的結果。
從上圖能夠看出,TypeScript 對 Decorator 編譯的結果跟 Babel 略微不一樣,TypeScript 對屬性和方法沒有過多的處理,惟一的區別可能就是在對類的處理上,傳入的 target
爲類自己,而不是 Prototype
。
不管是用什麼編譯器生成的代碼,最終參數仍是離不開 target, name, descriptor
。另外,不管怎麼包裝,最終也是爲了提供一個可以新增或者修改 descriptor
某個屬性的函數,只要是對屬性的修改,就必然離不開 Object.defineProperty
。
有時候,咱們難以讀懂某段代碼,可能只是由於沒有進入這段代碼的真實上下文(應用場景)。若是是按需求來開發某個 Decorator,事情就會變得簡單。
通用 Decorator,意味着將要用於生成具備共有特徵且用於不一樣場景的裝飾器,一般最容易讓人想到就是工廠模式。
咱們來看下 lodash-decorators
中的實現:
export class InternalDecoratorFactory {
createDecorator(config: DecoratorConfig): GenericDecorator {
// 基礎裝飾器
}
createInstanceDecorator(config: DecoratorConfig): GenericDecorator {
// 生成用於實例的裝飾器
}
private _isApplicable(
context: InstanceChainContext,
config: DecoratorConfig
): boolean {
// 是否可調用
}
private _resolveDescriptor(
target: Object,
name: string,
descriptor?: PropertyDescriptor
): PropertyDescriptor {
// 獲取 Descriptor 的通用方法。
}
}
複製代碼
這裏用 TypeScript 的好處在於,類自己具有某種結構。也就是可供類型描述使用。另外,在看源碼過程當中,TypeScript 的類型有助於快速理解做者意圖。
好比單看上面代碼,咱們就能夠知道 createDecorator
和 createInstanceDecorator
都接收類型爲 DecoratorConfig
的參數,以及返回都是通用的 Decorator GenericDecorator
。
那咱們先來看下:
export interface DecoratorConfigOptions {
bound?: boolean;
setter?: boolean;
getter?: boolean;
property?: boolean;
method?: boolean;
optionalParams?: boolean; // 是否使用自定義參數
}
export class DecoratorConfig {
constructor( public readonly execute: Function, // 處理函數,如傳入 debounce 函數 public readonly applicator: Applicator, // 根據處理函數不一樣,選用不一樣的函數調用程序。 public readonly options: DecoratorConfigOptions = {} ) {}
}
複製代碼
關鍵的參數有:
execute
裝飾函數的核心處理函數。applicator
主要做用是用於配置參數及函數的調用。options
額外的配置選項,如是不是屬性,是不是方法,是否使用自定義參數等。這裏的 Applicator 屬於函數調用中公共部分的抽離:
export interface ApplicateOptions {
config: DecoratorConfig;
target: any;
value: any;
args: any[];
instance?: Object;
}
export abstract class Applicator {
abstract apply(options: ApplicateOptions): any;
}
複製代碼
一個通用的 Decorator 的核心部分差很少就這些了,但因爲筆者實際應用 Decorators 的地方很少,對於 lodash-decorators
源碼中爲何有 createDecorator
和 createInstanceDecorator
兩種生成方法,以及爲何要引入 weekMap
的緣由,一時也給不了很是準確的答案。createInstanceDecorator
也許是出於原型鏈考慮?由於實例,才能訪問原型鏈繼承後獲得的方法,之後有機會再單獨深刻。
但願有這方面研究的讀者能夠不吝賜教,筆者不勝感激。
結合 lodash
,關注點分離了。實現各類 decorators 在代碼實現上就變得很是簡單。好比,前端可能會常常用到的函數節流,函數防抖,delay。
import debounce = require('lodash/debounce');
import { PreValueApplicator } from './applicators';
const decorator = DecoratorFactory.createInstanceDecorator(
new DecoratorConfig(debounce, new PreValueApplicator(), { setter: true })
);
export function Debounce( wait?: number, options?: DebounceOptions ): LodashDecorator {
return decorator(wait, options);
}
複製代碼
經過調用 DecoratorFactory
生成通用的 decorator,實現各類裝飾器功能就只須要像上面同樣組織代碼便可。
另外像 Mixin
這種看似組合優於繼承的用法是一種對類的裝飾,能夠這麼去實現:
import assign = require('lodash/assign');
export function Mixin(...srcs: Object[]): ClassDecorator {
return ((target: Function) => { assign(target.prototype, ...srcs); return target; }) as any; } 複製代碼
更多的功能,筆者就再也不過多贅述。再講就變成 lodash 源碼解析了。有心的讀者能夠去觸類旁通了,或者直接看 lodash-decorators
源碼。畢竟我也是看它們源碼來學習的。
這麼草率的結束,也許意味着還有更多學習空間。
Decorators
涉及的知識並不難,關鍵在於如何巧妙運用。初期沒經驗,能夠學習筆者看些周邊庫,好比 lodash-decorators
。所謂的低侵入性,也只是視覺感官上的,不過確實多少能提升代碼的可讀性。
最後,前端路上,多用 【聞道有前後,術業有專攻】安慰本身,學習永無止境。 感謝閱讀,願君多采擷!