自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JavaScript 裝飾器進(jìn)入 stage 3,是時候了解了!

開發(fā) 前端
裝飾器在未了解之前,給人一種神秘的感覺,了解之后會發(fā)現(xiàn)它的語法并沒有那么復(fù)雜。它的一個優(yōu)點(diǎn)是在不改變原有代碼的基礎(chǔ)之上允許我們來擴(kuò)展一些新功能。裝飾器只適用于類,不支持函數(shù)。函數(shù)相對類來說,更容易做一些修飾,例如我們可以使用高階函數(shù)做一些包裝。

在一些優(yōu)秀的開源框架,如 Angular、Nest.js、Midway 中會看到一種常見的寫法 @符號 + 方法名,做為一個只有 JavaScript 經(jīng)驗(yàn)的開發(fā)者第一次看到這種寫法還是感覺挺新奇的。

一個 @符號開頭 + 方法名,這種寫法正是 JavaScript 中的裝飾器 Decorators,它附加在我們定義的類、類方法、類字段、類訪問器、類自動訪問器上,在不改變原對象基礎(chǔ)上,為我們的程序增強(qiáng)一些功能,例如邏輯復(fù)用、代碼解耦。

如下所示 @executionTime 是我們實(shí)現(xiàn)的裝飾器函數(shù),它會記錄被修飾方法的執(zhí)行耗時,下文中我們會進(jìn)行詳細(xì)的介紹。

class Person {
@executionTime
run() {}
}

通過本文你將學(xué)到如下內(nèi)容

圖片

裝飾器動態(tài)

關(guān)于 JavaScript 裝飾器,已經(jīng)提案很多年了,盡管提案還未進(jìn)入最后階段,得益于 TypeScript 和 babel,我們可以在一些框架中提前使用到該技術(shù)。 先了解下裝飾器的最新動態(tài),及在 TypeScript、babel 中的支持程度。

一個好消息是 2022-03 JavaScript 裝飾器提案終于進(jìn)入到 stage 3 了,這已經(jīng)是到了候選階段了,該階段會接收使用者的反饋并對提案完善,離最終的 stage 4 最近的一個階段了。查看進(jìn)入到 TC39 提案的一些語法,參考這里閱讀更多 ECMAScript proposals。

一個提案進(jìn)入到 stage 3 之后,TypeScript 就會去實(shí)現(xiàn)它。裝飾器是一個特殊的存在,目前 TypeScript 中的裝飾器實(shí)現(xiàn)還是基于第一版裝飾器的提案(這在早期對 TypeScript 也是一個賣點(diǎn)了),和現(xiàn)在 stage 3 中的提案存在很大的差別。

做為用戶可能會擔(dān)心,會不會出現(xiàn)不兼容的情況?兼容肯定是要的,舊版也不會立馬廢棄,現(xiàn)在是通過 --experimentalDecorators?、--emitDecoratorMetadata 兩個配置開啟裝飾器,stage 3 的提案離最終已經(jīng)很近了,要么默認(rèn)開啟,要么在增加一個新的配置開啟,對使用者來說也許不會有太大的差異,對于庫的開發(fā)者 目前 TS 裝飾器和 JS 最新的裝飾器提案實(shí)現(xiàn)已經(jīng)不是一回事了,有很多問題需要考慮。

在可預(yù)見的未來,TypeScript 下個版本 5.0 大概率會實(shí)現(xiàn)新版裝飾器,參考 TypeScript 接下來的 5.0 版本迭代計劃,里面提到了 “實(shí)現(xiàn)更新后的 JavaScript 裝飾器提案”。

  • TypeScript 5.0 Iteration Plan #51362
  • Implement the updated JS decorators proposal #48885

babel 也一直在跟進(jìn) JavaScript 裝飾器提案,最新的 stage 3 版本也已實(shí)現(xiàn),算是支持的比較好的了,通過 @babel/plugin-proposal-decorators 插件提供支持,裝飾器不同階段的提案 babel 都有實(shí)現(xiàn),如下所示:

  • 2022-03:在 2022 年 3 月的 TC39 會議上就 Stage 3 達(dá)成共識的提案版本。閱讀更多相關(guān)信息tc39/proposal-decorators@8ca65c046d。
  • 2021-12:2021 年 12 月提交給 TC39 的提案版本。閱讀更多相關(guān)信息 tc39/proposal-decorators@d6c056fa06。
  • 2018-09:最初提升到第 2 階段的提案版本,于 2018 年 9 月提交給 TC39。閱讀更多相關(guān)信息tc39/proposal-decorators@7fa580b40f。
  • legacy:最初的第 1 階段提案,閱讀更多相關(guān)信息 wycats/javascript-decorators@e1bf8d41bf。

下文用到的所有的代碼會用 babel 編譯,接下來讓我們先搭建一個支持裝飾器的運(yùn)行環(huán)境。

搭建裝飾器運(yùn)行環(huán)境

babel 7.19.0 版本已支持 stage 3 裝飾器,安裝的 babel 大于此即可。

創(chuàng)建一個項目 decorators-demo

$ mkdir decorators-demo
$ cd decorators-demo
$ npm init

安裝 babel 依賴

npm i @babel/core @babel/cli @babel/preset-env @babel/plugin-proposal-decorators -D

創(chuàng)建一個名為 .babelrc 的新文件配置 babel

{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2022-03" }] // 這個地方要配置,因?yàn)楫?dāng)前默認(rèn)的版本不是最新的
]
}

編輯 package.json scripts 命令

"scripts": {
"build": "babel index.js -d dist",
"start": "npm run build && node dist/index.js"
}

類裝飾器

類裝飾器修飾的是定義的類本身,我們可以為它添加屬性、方法。類裝飾器類型簽名如下所示,第一個參數(shù) value 就是被修飾的類。

類型簽名如下所示:

type ClassDecorator = (value: Function, context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () void): void;
}) => Function | void;

為類擴(kuò)展一個新方法

例如,再不改變原始代碼的情況下,借助外部組件使用類裝飾器為 Person 類擴(kuò)展一個方法 run。

function addRunMethod(value, { kind, name }) {
if (kind === "class") {
return class extends value {
constructor(...args) {
super(...args);
}
run() {
console.log(`Add a run method for ${this.name}`);
}
}
}
}

@addRunMethod
class Person {
constructor(name) {
this.name = name
}
}

const p1 = new Person('Tom')
p1.run()
const p2 = new Person('Jack')
p2.run()

以上我們是通過繼承被修飾的類并返回一個新的子類方式來擴(kuò)展一個新方法。我們定義的類裝飾器函數(shù) addRunMethod 第一個參數(shù)既然是被修飾的類本身,因此我們還可以通過原型方式來擴(kuò)展一個新的方法或?qū)傩浴?/p>

function addRunMethod(value, { kind }) {
if (kind === "class") {
value.prototype.run = function () {
console.log(`Add a run method for ${this.name}`);
}
}
}

類方法裝飾器

類方法裝飾器接收一個被修飾的方法做為第一個參數(shù),并可以選擇返回一個新方法替換原始的方法。類型簽名如下所示:

type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;

下面從一個記錄方法執(zhí)行耗時的例子,做個不用裝飾器與用裝飾器的前后效果對比。日常開發(fā)工作中我們可能會有類似的需求:“記錄一個方法的執(zhí)行時間,便于后續(xù)做些性能優(yōu)化”。

示例:方法的計算耗時實(shí)現(xiàn)

下面定義了一個 Person 類,有一個 run(跑步) 方法,如果已經(jīng)吃過飯了,他就有更多的力氣,跑的就會快些,否則就會跑的慢些。sleep 方法是為了輔助做些延遲測試。

const sleep = ms new Promise(resolve setTimeout(() resolve(1), ms));
class Person {
async run(isEat) {
const start = Date.now();
if (isEat) {
await sleep(1000 * 1);
console.log(`execution time: ${Date.now() - start}`);
return `I can run fast.`;
}

await sleep(1000 * 5);
console.log(`execution time: ${Date.now() - start}`);
return `I can't run any faster.`;
}
}

const p = new Person();
console.log(await p.run(true))

上面代碼暴露出兩個問題:

  • 代碼耦合:計算方法執(zhí)行耗時邏輯與業(yè)務(wù)代碼耦合,如果測試沒問題之后,想要移除這部分計算執(zhí)行耗時的代碼邏輯,是不是就很難?
  • 重復(fù)性代碼:一個方法內(nèi),代碼 7 行、12 行,計算最后耗時部分是完全重復(fù)的邏輯,如果還有一個別的方法是不是也要在寫一遍?

示例:使用裝飾器計算耗時

接下來使用類方法裝飾器改寫以上示例,類方法裝飾器第一個參數(shù)為被裝飾的函數(shù),可以返回一個新函數(shù)來代替被裝飾的方法, 類型簽名如下所示:

type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () void): void;
}) => Function | void;

以下聲明一個 @executionTime? 裝飾器函數(shù),該函數(shù)包裝原始函數(shù),并在調(diào)用前后記錄函數(shù)執(zhí)行耗時。

現(xiàn)在的代碼邏輯看起來是不是清晰了很多?通過裝飾器函數(shù)擴(kuò)展了原有的功能,但并沒有修改原始函數(shù)內(nèi)容,如果哪天想去掉計算函數(shù)執(zhí)行耗時這段邏輯,也容易的多了。

const sleep = ms new Promise(resolve setTimeout(() resolve(1), ms));
function executionTime(value, context) {
const { kind, name } = context;
if (kind === 'method') {
return async function (...args) { // 返回一個新方法,代替被裝飾的方法
const start = Date.now();
const result = await value.apply(this, args);
console.log(`CALL method(${name}) execution time: ${Date.now() - start}`);
return result;
};
}
}

class Person {
@executionTime
async run(isEat) {
if (isEat) {
await sleep(1000 * 1); // 1 minute
return `I can run fast.`;
}

await sleep(1000 * 5); // 5 minute
return `I can't run any faster.`;
}
}

const p = new Person();
console.log(await p.run(true))

注意,類的構(gòu)造函數(shù)不支持裝飾器,盡管構(gòu)造函數(shù)看起來像方法,但實(shí)際上不是方法。

示例:實(shí)現(xiàn) @bind 解決 this 問題

下面代碼直接執(zhí)行 p.run() 是沒問題的,但是將 p.run 提取為一個方法再執(zhí)行,此時函數(shù)內(nèi)的 this 執(zhí)行就發(fā)生變化了,會被指向?yàn)?nbsp;undefined,單獨(dú)運(yùn)行 run() 方法后就會出現(xiàn) TypeError: Cannot read properties of undefined (reading '#name') 錯誤。

class Person {
#name
constructor(name) {
this.#name = name;
}

async run() {
console.log(`My name is ${this.#name}`);
}
}

const p = new Person('Tom');
const run = p.run;
run()

裝飾器上下文對象上提供了一個 [添加初始化邏輯 addInitializer()](https://github.com/tc39/proposal-decorators#adding-initialization-logic-with-addinitializer) 方法,可以調(diào)用此方法將初始化函數(shù)與類或類元素相關(guān)聯(lián),該方法在類實(shí)例化時會觸發(fā),允許用戶在初始化時添加一些額外的邏輯。

例如,在這里我們可以聲明一個 @bind? 裝飾器在 addInitializer() 方法觸發(fā)時將類方法綁定到類實(shí)例,解決 this 綁定問題。

function bind(value, context) {
const { kind, name, addInitializer } = context;
if (kind === 'method') {
addInitializer(function () {
this[name] = value.bind(this);
});
}
}

class Person {
...

@bind
async run() {
console.log(`My name is ${this.#name}`);
}
}

示例:實(shí)現(xiàn) @deprecated 提醒廢棄 API

對于某些在今后版本會被移除的 API,可以通過定義一個 @deprecated 的裝飾器,用于 API 將要廢棄的消息提醒。

const DEFAULT_MSG = 'This function will be removed in future versions.';
function deprecated(value, context) {
const { kind, name } = context;
if (kind === 'method') {
return function (...args) {
console.warn(`DEPRECATION ${name}: ${DEFAULT_MSG}`);
const result = value.apply(this, args);
return result;
};
}
}

class Person {
@deprecated
hello() {
console.log(`Hello world!`);
}
}

const p = new Person()
p.hello()

類訪問器裝飾器

類訪問器裝飾器與類方法裝飾器相似,區(qū)別地方在于上下文 context 對象的 kind 屬性為 getter、setter。

類型簽名如下所示:

type ClassGetterDecorator = (value: Function, context: {
kind: "getter" | "setter";
name: string | symbol;
access: { get(): unknown } | { set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;

示例:實(shí)現(xiàn) @logged 裝飾器

下例,實(shí)現(xiàn)一個 @logged 裝飾器 追蹤函數(shù)被調(diào)用的前后記錄,細(xì)看是不是同上面我們自定義的類方法裝飾器 @executionTime 很相似。

function logged(value, context) {
const { kind, name } = context;
if (kind === 'method' || kind === 'getter' || kind === 'setter') {
return function (...args) { // 返回一個新方法,代替被裝飾的方法
console.log(`starting ${name} with arguments ${args.join(", ")}`);
const result = value.apply(this, args);
console.log(`ending ${name}`);
return result;
};
}
}

class Person {
#name
constructor(name) {
this.#name = name;
}
@logged
get name() {
return this.#name;
}
@logged
set name(name) {
this.#name = name;
}
}

const p = new Person('Tom')
p.name = 'Jack'
p.name

類自動訪問裝飾器

讓我們先拋開裝飾器的概念,了解下 accessor 是什么?

類新成員:自動訪問器

類自動訪問器(Auto-Accessors)是 ECMAScript 將要推出的一個新功能,目前 TC39 提案為 stage 1。通過 ??accessor?? 關(guān)鍵詞在類屬性前定義。

class Person {
accessor name = 'Tom';
}

類自動訪問器相當(dāng)于在類原型上定義了 getter、setter,大致等價于以下行為:

class Person {
#name = 'Tom';
get name() {
return this.#name;
}
set name(name) {
this.#name = name;
}
}

自動訪問器裝飾器

自動訪問器裝飾器接收的第一個參數(shù) value 包含了在類原型上定義的訪問器對象 get、set,第二參數(shù) context 對象上的 kind 屬性為 accessor。

自動訪問器裝飾器允許攔截對屬性的訪問,在進(jìn)行一些包裝之后,返回一個新的 **get**、**set**? 方法,還支持返回一個 **init** 函數(shù),用于更改初始值

類型簽名如下所示:

type ClassAutoAccessorDecorator = (
value: {
get: () unknown;
set(value: unknown) => void;
},
context: {
kind: "accessor";
name: string | symbol;
access: { get(): unknown, set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}
) => {
get?: () => unknown;
set?: (value: unknown) => void;
init?: (initialValue: unknown) => unknown;
} | void;

示例:實(shí)現(xiàn) @readOnly

實(shí)現(xiàn)一個 @readOnly 裝飾器,確保被裝飾的屬性在第一次初始化后可以變?yōu)橹蛔x的,如果未被初始化不允許訪問該屬性,給一個錯誤提示。

const UNINITIALIZED = Symbol('UNINITIALIZED');
function readOnly(value, context) {
const { get, set } = value;
const { name, kind } = context;
if (kind === 'accessor') {
return {
init(initialValue) {
return initialValue || UNINITIALIZED;
},
get() {
const value = get.call(this);
if (value === UNINITIALIZED) {
throw new TypeError(
`Accessor ${name} hasn’t been initialized yet`
);
}
return value;
},
set(newValue) {
const oldValue = get.call(this);
if (oldValue !== UNINITIALIZED) {
throw new TypeError(
`Accessor ${name} can only be set once`
);
}
set.call(this, newValue);
},
};
}
}

class Person {
@readOnly
accessor name = 'Tom';
}

const p = new Person()
console.log(p.name);
// p.name = 'Jack' // TypeError: Accessor name can only be set once

類字段裝飾器

類字段裝飾器接收到的第一個參數(shù) value 為 undefined,我們不能更改字段的名稱。通過返回函數(shù),我們可以接收字段的初始值并返回一個新的初始值。

類型簽名如下:

type ClassFieldDecorator = (value: undefined, context: {
kind: "field";
name: string | symbol;
access: { get(): unknown, set(value: unknown): void };
static: boolean;
private: boolean;
}) => (initialValue: unknown) => unknown | void;

示例:將字段初始值翻倍

聲明一個 @multiples() 裝飾器,接收一個倍數(shù),將字段初始值翻倍處理并返回。

function multiples(mutiple) {
return (value, { kind, name }) => {
if (kind === 'field') {
return initialValue initialValue * mutiple;
}
}
}
class Person {
@multiples(3)
count = 2;
}
const p = new Person();
console.log(p.count); // 6

示例:依賴注入示例

Logger 是我們的基礎(chǔ)類,Person 類似于我們的業(yè)務(wù)類。以下代碼耦合的一點(diǎn)是 Person 與 Logger 產(chǎn)生了直接依賴,對于 Person 類只需要使用 Logger 實(shí)例,不需要把 Logger 實(shí)例化也放到 Person 類中。接下來我們要解決的是如何將兩個依賴模塊之間的初始化信息解耦。

class Logger {
info(...args) {
console.log(...args);
}
}

class Person {
logger = new Logger();

run() {
this.logger.info('Hi!', 'I am running.')
}
}
const p = new Person();
p.run();

依賴注入是將 “依賴” 注入給調(diào)用方,而不是讓調(diào)用方直接獲得依賴,在程序運(yùn)行過程中由專門的組件負(fù)責(zé) “依賴” 的實(shí)例化。

接下來,使用裝飾器和依賴注冊表實(shí)現(xiàn)依賴注入。

const assert = require('assert');
const { registry, inject } = createRegistry();

@registry.register('logger')
class Logger {
info(...args) {
console.log(new Date().toLocaleString(), ...args);
}
}

class Person {
@inject logger;
run() {
this.logger.info('Hi!', 'I am running.')
}
}

const p = new Person();
p.run();
assert.equal(p.logger, registry.getInstance('logger'));

下面是核心方法 createRegistry() 的實(shí)現(xiàn),參考了 JavaScript metaprogramming with the 2022-03 decorators API 的一個示例,下面主要用到了類裝飾器、類字段裝飾器。

function createRegistry() {
const classMap = new Map();
const instancesMap = new Map();
const registry = {
register(registerName) {
return (value, { kind }) => {
if (kind === 'class') {
classMap.set(registerName, value)
}
}
},
getInstance(name) {
if (instancesMap.has(name)) {
return instancesMap.get(name);
}

const TargetClass = classMap.get(name);
if (!TargetClass) {
throw new Error(`Unregistered dependencies with register name: ${name}`);
}
const instance = new TargetClass();
instancesMap.set(name, instance);
return instance;
},
}

return {
registry,

inject: (_value, {kind, name}) => {
if (kind === 'field') {
return () registry.getInstance(name);
}
}
};
}

總結(jié)

裝飾器在未了解之前,給人一種神秘的感覺,了解之后會發(fā)現(xiàn)它的語法并沒有那么復(fù)雜。它的一個優(yōu)點(diǎn)是在不改變原有代碼的基礎(chǔ)之上允許我們來擴(kuò)展一些新功能。

裝飾器只適用于類,不支持函數(shù)。函數(shù)相對類來說,更容易做一些修飾,例如我們可以使用高階函數(shù)做一些包裝。

裝飾器可以應(yīng)用于,類、類字段、類方法、類訪問器、類自動訪問器(這是類的一個新成員)。掌握了裝飾器的使用之后,再去看像 Nest.js、Angular 等這些框架時對于 @expression 這種語法,不會再陌生了。

在了解了裝飾器后,下一步讓我們在詳細(xì)了解下什么是 IoC(控制反轉(zhuǎn))、DI(依賴注入)。

責(zé)任編輯:武曉燕 來源: 編程界
相關(guān)推薦

2025-02-17 08:18:27

C#TypeScriptJavaScript

2021-01-12 18:39:24

開源軟件OSS開發(fā)

2023-10-19 15:25:40

2021-06-03 09:18:25

裝飾器模式包裝

2020-04-14 12:12:20

JavaScriptIIFE函數(shù)

2023-11-06 13:08:45

2020-01-18 14:34:40

5G技術(shù)通信

2023-11-06 17:37:17

技術(shù)架構(gòu)任務(wù)隊列

2019-05-07 10:03:47

Linux系統(tǒng)發(fā)行版

2010-08-29 21:09:57

DHCP協(xié)議

2023-12-14 12:55:41

Pythondel語句

2024-02-19 08:40:22

2023-09-27 16:29:55

開發(fā)團(tuán)隊信息

2023-11-27 00:48:46

displayvisibility

2023-06-26 07:32:43

Kubernetes容器

2024-01-03 08:08:51

Pulsar版本數(shù)據(jù)

2024-03-20 08:31:40

KotlinExtension計算

2023-05-09 09:00:20

版本Canary框架

2020-09-03 07:27:16

自然語言處理NLP語言

2023-11-29 13:51:00

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號