當class properties遇上decorator
本篇共3個章節。
前2個章節介紹class中兩種方式定義方法的不同、decorator如何作用於class的方法。
最後1個章節通過一個demo介紹瞭如何實現一個相容class普通方法和class屬性方法的裝飾器,以及如何保留裝飾器裝飾的箭頭函式式中this為類例項的特性。
一、class中的函式
在React中的函式中固定this指向元件例項是一個常見的需求,通常有以下三種寫法:
1.在constructor中使用bind指定this:
this.handlePress = this.handlePress.bind(this) 複製程式碼
2.使用autobind的裝飾器:
@autobind handlePress(){} 複製程式碼
3.使用class properties與arrow function
handlePress = () => {} 複製程式碼
這裡有兩種為類宣告方法的方式,第一種如1、2在類中直接宣告方法,第二種為將方法宣告為類的一個屬性(’=‘標識)。
我們都知道class即function,讓我們定義一個簡單的類,觀察babel編譯後的結果,看看這兩種方式宣告的方法有何不同。
class A { sayHello() { } sayWorld = function() { } } 複製程式碼
編譯後
var A = function () { function A() { _classCallCheck(this, A); this.sayWorld = function () {}; } _createClass(A, [{ key: "sayHello", value: function sayHello() {} }]); return A; }(); 複製程式碼
編譯後的程式碼中sayHello和sayWorld是通過不同方式關聯到A上的。sayWorld的定義發生在建構函式執行期間,即類例項的建立時。而sayHello是通過_createClass方法關聯到A上的。
來看看_createClass做了什麼:
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { // 建立一個數據屬性,並將其定義在target物件上 var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 複製程式碼
_createClass中建立了一個如下的資料屬性,使用Object.defineProperty定義在A.prototype上。
{ enumerable: false, configurable: true, writable: true, value: function sayHello() {} } 複製程式碼
可見sayHello方法是定義在A.prototype上的方法,會被眾多A的例項所共享;而sayWorld則是每個A例項獨有的方法(每次建立例項都會新建)。
得出結論:
1、普通的類方法實際歸屬於class.prototype,該類的眾多例項將通過原型鏈共享該方法。
2、屬性方式定義的類方法歸屬於class的例項,同名方法在類的不同例項中並不相同。
讓我們對A做一些修改,重新編譯。
class A { sayHello() { console.log('hello', this); } sayWorld = function() { console.log('world', this); } sayName = () => { console.log('name', this); } } 複製程式碼
編譯後
var A = function () { function A() { var _this = this; _classCallCheck(this, A); this.sayWorld = function () { console.log('world', this); }; this.sayName = function () { console.log('name', _this); }; } _createClass(A, [{ key: 'sayHello', value: function sayHello() { console.log('hello', this); } }]); return A; }(); 複製程式碼
我們都知道箭頭函式中this的指向為其宣告時當前作用域的this,所以sayName中的this在編譯過程中被替換為_this(建構函式執行時的this,即類例項本身),這就是前面固定方法this指向例項的第三種方法"使用class properties與arrow function"生效的原因。
二、decorator
裝飾器(decorator)是一個函式,用於改造類與類的方法。篇幅原因我們這裡只介紹作用於類方法的裝飾器。一個簡單的函式裝飾器構造如下:
function decoratorA(target, name, descriptor) { // 未做任何修改 } 複製程式碼
-
target為class.prototype。
-
name即方法名稱。
-
descriptor有兩種,資料屬性和訪問器屬性。兩種屬性包含了6種特性,enumerable和configurable為共有的2種特性,writable和value為資料屬性獨有,而getter和setter為訪問器屬性獨有。
看一個簡單的例子:
function decoratorA() {} function decoratorB() {} class A { @decoratorA @decoratorB sayHello() { } } 複製程式碼
編譯後
function decoratorA() {} function decoratorB() {} var A = (_class = function () { function A() { _classCallCheck(this, A); } _createClass(A, [{ key: "sayHello", value: function sayHello() {} }]); return A; }(), (_applyDecoratedDescriptor(_class.prototype, "sayHello", [decoratorA, decoratorB], Object.getOwnPropertyDescriptor(_class.prototype, "sayHello"), _class.prototype)), _class); 複製程式碼
與之前一樣sayHello定義為A.prototype的屬性,而後執行_applyDecoratedDescriptor應用裝飾器decoratorA和decoratorB。
來看看_applyDecoratedDescriptor做了什麼:
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object['ke' + 'ys'](descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } // 以上為初始化一個數據屬性(initializer不屬於上文提到的6種屬性特性,第三節詳述其作用) // 本例中此處desc為{ enumerable: false, configurable: true, writable: true, value: function sayHello() {} } // 此處的reverse表明裝飾器將按照距離sayHello由近及遠的順序執行,即先應用decoratorB再應用decoratorA desc = decorators.slice().reverse().reduce(function (desc, decorator) { // 裝飾器執行,可在裝飾器內部按需修改desc return decorator(target, property, desc) || desc; }, desc); // 本例中無initializer不執行此段程式碼 if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { // 將裝飾器處理後的desc定義到target即A.prototype上 Object['define' + 'Property'](target, property, desc); desc = null; } // 返回null return desc; } 複製程式碼
通過上述程式碼分析我們認識到:
1、裝飾器的執行發生在類建立後,此時並無例項
2、依照距離函式由近及遠執行
3、通過修改被裝飾方法的屬性特性,可以實現我們所需的功能(例如autobind-decorator實現繫結this)。
三、當class properties遇到decorator
decorator是es7納入規範的js特性,而class properties目前是stage3階段(截止2018.11.23)的提案,還沒有正式納入ECMAScript。
一個屬性方法的特點是其建立在例項生成階段(建構函式中),而裝飾器的執行是在類建立後(例項生成前),這裡就發生了一個概念上的小衝突,裝飾器執行時屬性方法似乎還沒建立。那裝飾器是如何裝飾一個屬性方法的呢,讓我們到程式碼中找出答案。
function decoratorA() {} class A { @decoratorA sayName = () => { console.log(this); } } 複製程式碼
編譯後
function _initDefineProp(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); } function decoratorA() {} var A = (_class = function A() { _classCallCheck(this, A); _initDefineProp(this, "sayName", _descriptor, this); }, (_descriptor = _applyDecoratedDescriptor(_class.prototype, "sayName", [decoratorA], { enumerable: true, initializer: function initializer() { var _this = this; return function () { console.log(_this); }; } })), _class); 複製程式碼
簡單的描述:
1、通過initializer來記錄並標識類的屬性方法
2、_applyDecoratedDescriptor建立返回了一個屬性的描述物件_descriptor
3、在建構函式中通過_initDefineProp將_descriptor定義到例項this上(屬性方法依然歸屬於例項,而不是class.prototype)
從_initDefineProp逆推,有2個關鍵點需要注意:
1、_applyDecoratedDescriptor需返回一個包含initializer的descriptor,以確保屬性的value是通過initializer呼叫初始化
2、裝飾器在處理descriptor時,返回的descriptor需包含initializer,而不是資料屬性或訪問器屬性格式的descriptor.
實現一個相容普通類函式和類屬性函式的裝飾器(保留箭頭函式的this繫結)
需求:檢查登入狀態的裝飾器,當裝飾器修飾的方法呼叫時,檢查登入狀態。若已登入則執行該方法,若未登入,則執行一個指定方法提示需登入。
// 登入狀態 let logined = true; function checkLoginStatus() { return new Promise((resolve) => { resolve(logined); // 每次返回登入狀態後對登入狀態取反 logined = !logined; }); } // 提示需要登入 function notice(target, tag) { console.log(tag, this === target, 'Need Login!'); } // 檢查登入狀態的裝飾器 function checkLogin(notLoginCallback) { return function decorator(target, name, descriptor) { // 方法為類屬性方法 if (descriptor.initializer) { const replaceInitializer = function replaceInitializer() { const that = this; // 此處傳入了指向類例項的this const fn = descriptor.initializer.call(that); return function replaceFn(...args) { checkLoginStatus().then((login) => { if (login) { return fn.call(this, ...args); } return notLoginCallback.call(this, ...args); }); }; }; return { enumerable: true, configurable: true, writable: true, initializer: replaceInitializer, }; } // 普通的類方法 const originFn = descriptor.value; const replaceFn = function replaceFn(...args) { const that = this; checkLoginStatus().then((login) => { if (login) { return originFn.call(that, ...args); } return notLoginCallback.call(that, ...args); }); }; return { enumerable: true, configurable: true, writable: true, value: replaceFn, }; } } class A { constructor() { this.printA2 = this.printA2.bind(this); } printA1(target, tag) { console.log(tag, this === target); } @checkLogin(notice) printA2(target, tag) { console.log(tag, this === target); } printB1 = function(target, tag) { console.log(tag, this === target); } @checkLogin(notice) printB2 = function(target, tag) { console.log(tag, this === target); } printC1 = (target, tag) => { console.log(tag, this === target); } @checkLogin(notice) printC2 = (target, tag) => { console.log(tag, this === target); } } const a = new A(); a.printA1(a, 1);// 1 true (0, a.printA1)(a, 2);// 2 false a.printA2(a, 3);// 3 true (0, a.printA2)(a, 4);// 4 true 'Need Login!' a.printB1(a, 5);// 5 true (0, a.printB1)(a, 6);// 6 false a.printB2(a, 7);// 7 true (0, a.printB2)(a, 8);// 8 false 'Need Login!' a.printC1(a, 9);// 9 true (0, a.printC1)(a, 10);// 10 true a.printC2(a, 11);// 11 true (0, a.printC2)(a, 12);// 12 true 'Need Login!' 複製程式碼