裝飾器本質上提供了對被裝飾對象 Property Descriptor 的操作,在運行時被調用。
因為對於同一對象來說,可同時運用多個裝飾器,然后裝飾器中又可對被裝飾對象進行任意的修改甚至是替換掉實現,直觀感覺會有一些主觀認知上的錯覺,需要通過代碼來驗證一下。
比如,假若每個裝飾器都對被裝飾對象的有替換,其結果會怎樣?
多個裝飾器的應用
通過編譯運行以下示例代碼並查看其結果可以得到一些直觀感受:
function f() {
console.log("f(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[f]before ${key} called`, args);
const result = original.apply(this, args);
console.log(`[f]after ${key} called`);
return result;
};
console.log("f(): called");
return descriptor;
};
}
function g() {
console.log("g(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(</span>[g]before ${<span class="pl-smi">key</span>} called<span class="pl-pds">, args);
const result = original.apply(this, args);
console.log(</span>[g]after ${<span class="pl-smi">key</span>} called<span class="pl-pds">);
return result;
};
console.log("g(): called");
return descriptor;
};
}
class C {
@f()
@g()
foo(count: number) {
console.log(</span>foo called ${<span class="pl-smi">count</span>}<span class="pl-pds">);
}
}
const c = new C();
c.foo(0);
c.foo(1);
先放出執行結果:
f(): evaluated
g(): evaluated
g(): called
f(): called
[f]before foo called [ 0 ]
[g]before foo called [ 0 ]
foo called 0
[g]after foo called [ 0 ]
[f]after foo called [ 0 ]
[f]before foo called [ 1 ]
[g]before foo called [ 1 ]
foo called 1
[g]after foo called [ 1 ]
[f]after foo called [ 1 ]
下面來詳細分析。
編譯后的裝飾器代碼
首頁看看編譯后變成 JavaScript 的代碼,畢竟這是實際運行的代碼:
編譯后的代碼
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function f() {
console.log("f(): evaluated");
return function (_target, key, descriptor) {
var original = descriptor.value;
descriptor.value = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[f]before " + key + " called", args);
var result = original.apply(this, args);
console.log("[f]after " + key + " called", args);
return result;
};
console.log("f(): called");
return descriptor;
};
}
function g() {
console.log("g(): evaluated");
return function (_target, key, descriptor) {
var original = descriptor.value;
descriptor.value = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[g]before " + key + " called", args);
var result = original.apply(this, args);
console.log("[g]after " + key + " called", args);
return result;
};
console.log("g(): called");
return descriptor;
};
}
var C = /** @class */ (function () {
function C() {
}
C.prototype.foo = function (count) {
console.log("foo called " + count);
};
__decorate([
f(),
g(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], C.prototype, "foo", null);
return C;
}());
var c = new C();
c.foo(0);
c.foo(1);
先看經過 TypeScript 編譯后的代碼,重點看這一部分:
var C = /** @class */ (function () {
function C() {
}
C.prototype.foo = function (count) {
console.log("foo called " + count);
};
__decorate([
f(),
g(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], C.prototype, "foo", null);
return C;
}());
tslib 中裝飾器的實現
其中 __decorate 為 TypeScript 經 tslib 提供的 Decorator 實現,其源碼為:
var __decorate =
(this && this.__decorate) ||
function(decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
裝飾器的執行順序
配合編譯后代碼和這里裝飾器的實現來看,進一步之前了解到的關於裝飾器被求值和執行的順序,
源碼中應用裝飾器的地方:
@f()
@g()
foo(count: number) {
console.log(`foo called ${count}`);
}
然后這里的 @f() @g() 按照該順序傳遞給了 __decorate 函數,
__decorate(
[
+ f(),
+ g(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
],
C.prototype,
"foo",
null
);
然后在 __decorate 函數體中,對傳入的 decorators 從數據最后開始,取出裝飾器函數順次執行,
var __decorate =
(this && this.__decorate) ||
function(decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
+ for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
其中 r 便是裝成器的返回,會被當作被裝飾對象的新的屬性描述器(Property Descriptor)來重新定義被裝飾的對象:
Object.defineProperty(target, key, r)
所以,像示例代碼中多個裝飾器均對被裝飾對象有修改,原則上和多次調用 Object.defineProperty() 相當。
Object.defineProperty()
而調用 Object.defineProperty() 的結果是后面的會覆蓋前面的,比如來看這里一個簡單的示例:
const obj = {};
Object.defineProperty(obj, "foo", {
configurable: true,
value: function() {
console.log("1");
}
});
Object.defineProperty(obj, "foo", {
value: function() {
console.log("2");
}
});
obj.foo(); // 2
注意: 根據 MDN 對 defineProperty 的描述,configurable 在缺省時為 false,所以如果要重復定義同一個 key,需要顯式將其置為 true。
configurable
trueif and only if the type of this property descriptor may be changed and if the > property may be deleted from the corresponding object.
Defaults tofalse.
回到本文開頭的示例,為了進一步驗證,可通過將運用裝飾之后的屬性描述器打印出來:
console.log(Object.getOwnPropertyDescriptor(C.prototype, "foo").value.toString());
輸出結果為:
function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[f]before " + key + " called", args);
var result = original.apply(this, args);
console.log("[f]after " + key + " called", args);
return result;
}
那么這里引出另一個問題,通過裝飾器重復定義同一屬性時,並沒有顯式返回一個 configurable:true 的對象,那為何在運用多個裝飾器重復定義時沒報錯。
裝飾器入參中的 descriptor
答案就只有一個,那就是裝飾器傳入的 descriptor 已經是 configurable 為 true 的狀態。
為了驗證,只需要在 @f() 或 @g() 任意一個裝飾器中將 descriptor 打印出來即可。
function g() {
console.log("g(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
+ console.log(descriptor)
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[g]before ${key} called`, args);
const result = original.apply(this, args);
console.log(`[g]after ${key} called`, args);
return result;
};
console.log("g(): called");
return descriptor;
};
}
輸出的 descriptor:
{
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
這便是最終運行時會執行的 foo 方法真身。
可以看到確實是最后生效的裝飾器確實是后運用的 @f()。因此你確實可以這么理解多個裝飾器的重疊應用為,那一切都還說得通,就是 后運用的裝飾器中 對被裝飾對象的替換 會覆蓋掉 先運用的裝飾器 對被裝飾對象的替換。
But,
這解釋不了它的輸出結果:
f(): evaluated
g(): evaluated
g(): called
f(): called
[f]before foo called [ 0 ]
[g]before foo called [ 0 ]
foo called 0
[g]after foo called
[f]after foo called
[f]before foo called [ 1 ]
[g]before foo called [ 1 ]
foo called 1
[g]after foo called
[f]after foo called
裝飾器嵌套
原因就在於這句代碼:
var result = original.apply(this, args);
因為這句,@f() 和 @g() 便不是簡單的覆蓋關系,而是形成了嵌套關系。
這里 original 為 descriptor.value,即裝飾器傳入的 descriptor 的一個副本。我們在進行覆蓋前保存了一下原方法的副本,
// 保存原始的被裝飾對象
const original = descriptor.value;
// 替換被裝飾對象
descriptor.value = function(...args: any[]) {
// ...
}
因為裝飾器的目的只是對已有的對象進行修飾加強,所以你不能粗暴地將原始的對象直接替換成新的實現(當然你確實可以那樣粗暴的),那樣並不符合大多數應用場景。所以在進行替換時,先保存原始對象(這里原始對象是 foo 方法),然后在新的實現中對原始對象再進行調用,這樣來實現了對原始對象進行修飾,添加新的特性。
descriptor.value = function(...args: any[]) {
console.log(`[g]before ${key} called`, args);
+ const result = original.apply(this, args);
console.log(`[g]after ${key} called`, args);
return result;
};
通過這種方式,多個裝飾器對被裝飾對象的修改可以層層傳遞下去,而不至於丟失。
下面把每個裝飾器接收到的屬性描述器打印出來:
function f() {
console.log("f(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
+ console.log("[f] receive descriptor:", original.toString());
descriptor.value = function(...args: any[]) {
console.log(`[f]before ${key} called`, args);
const result = original.apply(this, args);
console.log(`[f]after ${key} called`, args);
return result;
};
console.log("f(): called");
return descriptor;
};
}
function g() {
console.log("g(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
+ console.log("[g] receive descriptor:", original.toString());
descriptor.value = function(...args: any[]) {
console.log([g]before ${key} called, args);
const result = original.apply(this, args);
console.log([g]after ${key} called, args);
return result;
};
console.log("g(): called");
return descriptor;
};
}
輸出結果:
[g] receive descriptor:
function (count) {
console.log("foo called " + count);
}
[f] receive descriptor:
function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[g]before " + key + " called", args);
var result = original.apply(this, args);
console.log("[g]after " + key + " called", args);
return result;
}
這里的示例中,先是 @g() 被調用,它接收到的 descriptor 就是原始的 foo 方法的屬性描述器,打印出其值便是原始的 foo 方法的方法體,
function (count) {
console.log("foo called " + count);
}
經過 @g() 處理后的屬性描述器傳遞給了下一個裝飾器 @f(),所以后者接收到的是經過處理后新的屬性描述器,即 @g() 返回的那個:
function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[g]before " + key + " called", args);
var result = original.apply(this, args);
console.log("[g]after " + key + " called", args);
return result;
}
然后將 @f() 中 original 替換成上述代碼便是最終 @f() 返回的最終 foo 的樣子,大致是這樣的:
descriptor.value = function(...args: any[]) {
console.log(`[f]before ${key} called`, args);
// g 開始
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[g]before " + key + " called", args);
// foo 開始
console.log(</span>foo called <span class="pl-s1"><span class="pl-pse">${</span>count<span class="pl-pse">}</span></span><span class="pl-pds">);
// foo 結束
console.log("[g]after " + key + " called", args);
// g 結束
console.log(</span>[f]after <span class="pl-s1"><span class="pl-pse">${</span>key<span class="pl-pse">}</span></span> called<span class="pl-pds">, args);
return result;
};
所以最終的 foo 方法其實是 f(g(x)) 兩者嵌套組合的結果,像數學上的函數調用一樣。
總結
多個裝飾器運用於同一對象時,其求值和執行順序是相反的,
對於類似這樣的調用:
@f
@g
x
- 求值順序是由上往下
- 執行順序是由下往上
通常情況下我們只關心執行順序,除非是在編寫復雜的裝飾器工廠方法時。同時需要注意到,這里所指的裝飾器執行順序 是裝飾器本身被調用的順序,如果是裝飾方法,這和 descriptor.value 被執行的順序是兩碼事,后者的執行是層層嵌套的方式,聯想 Koa 中間件的洋蔥圈模型。
如果多個裝飾器中都對被裝飾對象有所修改,注意嵌套過程中修改被覆蓋的問題,如果不想要產生覆蓋,裝飾器中應該有對被裝飾對象保存副本並且調用,方法通過 fn.apply(),類則可通過返回一個新的但繼承自被裝飾對象的新類來實現,比如:
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
這里覆蓋了被裝飾類的構造器,但其他未修改的部分仍是原來類中的樣子,因為這里返回的是一個 extends 后的新類。
