TypeScript中的裝飾器(Decorators)的本質是什麼(或者說它具體做了什麼工作)?


裝飾器(Decorator)並不是 TypeScript 提供的語言特性,僅僅是 TypeScript 所支持的 JavaScript/ECMAScript 語言提案。

實際上,裝飾器 可能是目前存活的語言提案中語義調整最大的一個,現有的提案與 TypeScript 目前(1.5.0~2.6.1)所實現的曾經的曾經的提案差別十分明顯,甚至和曾經的提案都有極大的差別。

先上結論:

  • TypeScript 所支持的裝飾器只是函數調用的語法糖;
  • 目前的裝飾器提案&不完全是&完全不是語法糖,甚至根本不可能被(正確地)轉譯實現。

TypeScript 引入裝飾器的歷史

首先,裝飾器在 TC39 的處理流程上並不是一個提案,而是多個相關聯的提案,包括:

  • Class Property Decorators(當前 Stage-2);
  • Function Expression Decorators(當前 Stage-0);
  • Method Parameter Decorators(當前 Stage-0);

其中 TypeScript 支持的是 Class Property Decorators(並不是當前版本)和 Method Parameter Decorators。不過大部分時候我們提到裝飾器可能都指代的是 Class Property Decorators。以下內容中為了描述方便,可能會有使用 裝飾器/Decorator 默認指代 Class Property Decorators 的情況。

註:近期 原裝飾器 提案被 新裝飾器提案 替換 後,Entry 的名稱由「Class and Property Decorators」調整為「Decorators」。

要知道,TypeScript 只會對 Stage-3 以上的語言提案提供支持,然而 TypeScript 引入裝飾器發生在 2015/03/18,而裝飾器在 2015/03/24 才進入 Stage-1,也就是說 TypeScript 當時引入的仍然是 Stage-0(2014/04/10)的提案,顯然不符合 TypeScript 的基本設定。

究其原因,可能大家都很清楚。在 NG-Conf 2015 上,Angular 團隊宣布與 TypeScript 團隊進行合作:

AtScript is TypeScript

這次 PY 交易的結果是:

  • Angular 團隊不再維護基於 Traceur 實現的 AtScript 語言;
  • TypeScript 引入一個類 Annotation 特性語法(及其擴展);

註:準確地說,語言是語言,實現是實現。不過由於 AtScript 至死都只有一個實現,這麼說著應該不會產生很大歧義。

仔細注意的話很容易發現,為了要能夠支持 Angular,TypeScript 引入的裝飾器特性並不是純粹的裝飾器提案,附加了:

  • 支持通過 emitDecoratorMetadata 選項暴露類構造函數的參數類型;
  • 支持針對類構造器/方法參數的裝飾器;
  • 支持類成員屬性的裝飾器;

註:估計肯定有人看不出來支持成員屬性有什麼不對。可是要知道,在 ES6 乃至當今 ES8 的 Class 中,Class(語法上)的 Member 都只能是方法或者訪問器屬性,根本不存在值屬性這個語法,對應的裝飾器當然一開始就無法存在。最新的裝飾器提案(本名就叫 Unified Class Features)才開始強綁定了另一個 Class Fields 提案(Stage-3)。

換句話說,這個特性從引入之初就是完全違背 TypeScript 自身的設計目標的。明確違反的有:

  • [Goal]: Impose no runtime overhead on emitted programs.
  • [Goal]: Align with current and future ECMAScript proposals.
  • [Goal]: Avoid adding expression-level syntax.
  • [Goal]: Use a consistent, fully erasable, structural type system.
  • [Non-goal]: Add or rely on run-time type information in programs, or emit different code based on the results of the type system.
  • [Non-goal]: Provide additional runtime functionality or libraries.

當然,雖然說引入裝飾器的過程並不符合 TypeScript 自身的定位,但是未來的道路還是要繼續向 ECMAScript 看齊的。於是,自從 TypeScript 2.1 開始,Roadmap 中的 Future 部分便多了一條:

Implement new ES Decorator proposal

然後至今沒有任何其它動作。(其實這裡 TypeScript 團隊確實沒做錯,因為裝飾器至今也還沒到 Stage-3)


TypeScript 中的舊裝飾器語義

曾經的裝飾器提案就是一個函數調用的語法糖,並無任何其它內容,例如:

@myFun
class myClass {}

(近似)等價於:

const tmp = class myClass {}
const myClass = myFun(tmp)

簡單地說就是把舊的 Class 轉換成了一個新的 Class。

對於屬性也有:

class myClass {
@myFun
myMethod() {}
}

(近似)等價於:

class myClass {}
const oldDescriptor = Object.getOwnPropertyDescriptor(myClass.prototype, "myMethod")
const newDescriptor = myFun(Foo.prototype, "myMethod", oldDescriptor)
Object.defineProperty(Foo.prototype, "myMethod", newDescriptor)

簡單地說就是把一個舊的 Property 轉換成了一個新的 Property。

註:很容易看出,由於裝飾器就是當作普通的表達式使用,因此括弧存在與否會產生完全不同的結果。

並沒有任何複雜的邏輯存在。


Angular 中對裝飾器的使用

事實上,Angular 使用的根本不是 裝飾器(Decorator),而是 註解(Annotation)

裝飾器的定位是通過對應的裝飾函數,修改內容本身的定義,從而實現不同的行為。而註解並不產生任何行為,僅僅添加附加內容,需要相應的 Scanner 讀取並識別其中的內容,從而使得 Scanner 自身產生不同的行為。

AtScript 語言中,@ 語法就是註解,用於添加對應的元數據;而在 TypeScript 語言中,@ 語法是裝飾器,用於攔截類型的定義過程。

例如,對於同樣的 AtScript 代碼:

@myFun
class myClass {}

生成的結果(近似)為:

class myClass {}
myClass.annotations = [
new myFun
]

註:很容易看出,由於註解都會通過 new 實例化,因此括弧存在與否不會產生影響。

這樣似乎沒有任何意義,為此我們傳遞一點信息:

@myFun(42)
class myClass {}

得到:

class myClass {}
myClass.annotations = [
new myFun(42)
]

這樣就可以在運行時通過(相當簡單的)反射獲取到 42 這個內容。

不過,既然在裝飾器中也可以獲取到 Class 的引用,當然也就可以實現「添加元數據」這個操作。例如將 myFun 實現為:

function myFun(value) {
return (clazz) =&> {
clazz.annotations = [
new myMetadata(value)
]
return clazz
}
}

這樣我們就可以通過同樣的反射代碼獲取到相同的元數據。

所以說,(在 JIT 編譯下)Angular 是通過裝飾器來模擬了註解的功能,因此看起來和裝飾器的功能完全不搭架。

註:對於 JIT 編譯而言,早期版本中使用自稱 Stage -1(Minus One,不是連字元)的 Metadata Reflection API 來添加元數據;而新版本中則直接通過對靜態屬性的賦值實現(早該這麼做了)。而對於 AOT 編譯而言,所有的元數據不會在運行時使用,可以被完全抹除(Build Optimizer 就是干這個的),討論存在形式沒有意義(雖然還是靜態屬性)。

綜上所述,想通過 Angular 框架學習裝飾器的使用完全是扯淡,Angular 中的裝飾器和裝飾器的(真正)作用幾乎沒有任何交集


新的裝飾器提案

寫在前面:目前沒有任何(新)裝飾器提案的實現,所以不管提案好不好,誰都一樣用不了。

新提案提出了 Class Descriptor 和 Element Descriptor 的概念。例如對於上面的類裝飾器:

@myFun
class myClass {
myMethod() { console.log(42) }
}

傳遞給 myFun 的內容不再是類的引用,而是一個對應的描述結構:

const oldClassDescriptor = {
kind: "class",
elements: [{
kind: "method",
key: "myMethod",
placement: "prototype",
descriptor: {
value: function() { console.log(42) },
enumerable: false,
configurable: true
}
}]
}
const newClassDescriptor = myFun(oldClassDescriptor)
const myClass = __constructClassByDescriptorSomehow__(newClassDescriptor)

也就是說源碼中看上去所謂的 Class 將不復存在,而是完全被拆解成構造所需信息,而裝飾器函數將對這份構造信息進行變換,之後通過新的構造信息在運行時重新創造一個 Class。(即任何地方都不存在舊的 Class,包括閉包里也沒有)

註:新的裝飾器提案中仍然可以實現註解,不過需要用到 finisher。

除此之外,新的裝飾器提案提供了對 Private Fields 的訪問支持,而 Private Fields 是 Hard Privacy,不能通過任何運行時手段訪問(比閉包還更為嚴格,考慮到 defineProperty 的特殊使用),因此實際上新的裝飾器是無法通過轉譯實現的(假設 Private Fields 本身是原生實現),當然目前 Private Fields 一樣沒有任何實現。

然而,對於大部分普通用戶而言,沒有實現的東西不論規範寫得再好也和他們沒有交集。

所以如果不是興趣使然沒必要特別關注,性價比並不高。


綜上,如果想要了解裝飾器這個語言提案(的真正內容),一定要忘掉 TypeScript 和 Angular 的存在


樓上洋洋洒洒回答了這麼多。。。

一言以蔽之:Aspect Oriented Programming. 十幾年前就熱炒的概念,如今TS搬出來炒炒冷飯,ES7也希望能夠加入規範。這是好事,JS圈終於又開始正視一種極具價值的開發模型了。


推薦閱讀:

在使用前端框架的同時,該如何提升自己原生JS的能力?
2016 年的今天,Web 前端框架是否已經同質化?
2017 年,前端的發展是否趨於平緩?

TAG:前端開發 | JavaScript | ECMAScript | TypeScript | Angular? |