JavaScriptの未来:デコレータ





良い一日、友達!



JavaScriptでのデコレータの使用に関する新しい提案(2020年9月)の適合翻訳を、何が起こっているかについて少し説明しながら、あなたの注意を喚起します。



この提案は約5年前に最初に行われ、それ以来、いくつかの重要な変更が加えられています。現在(まだ)検討の第2段階にあります。



デコレータのことを聞いたことがない場合、または知識を磨きたい場合は、次の記事を読むことをお勧めします。





では、デコレータとは何ですか?デコレータは、クラスの要素(フィールドまたはメソッド)またはその定義中にクラス自体で呼び出される関数であり、要素(またはクラス)を新しい値(デコレータによって返される)でラップまたは置換します。



装飾されたクラスフィールドは、ゲッター/セッターからのラッパーとして扱われ、そのフィールドに値を取得/割り当て(変更)できます。



デコレータは、クラスメンバーにメタデータで注釈を付けることもできます。メタデータは、デコレータによって追加された単純なオブジェクトプロパティのコレクションです。これらは、[Symbol.metadata]プロパティでネストされたオブジェクトのセットとして使用できます。



構文



デコレータ構文は、@(@ decoratorName)プレフィックスに加えて、次のことを前提としています。



  • デコレータ式は、変数チェーン(複数のデコレータを使用できます)、プロパティへのアクセスは。、ただし[]は使用できず、()を使用した呼び出しに制限されています。
  • クラス定義だけでなく、それらの要素(フィールドとメソッド)も装飾できます。
  • クラスデコレータは、エクスポートおよびデフォルトの後に指定されます


デコレータを定義するための特別なルールはありません。任意の機能をそのまま使用できます。



セマンティクスの詳細



デコレータは3つのステップで評価されます。



  1. デコレータ式(@に続くもの)は、計算されたプロパティ名とともに評価されます
  2. デコレータは、クラス定義中、メソッドが評価された後、コンストラクタとプロトタイプが結合される前に(関数として)呼び出されます。
  3. デコレータは、呼び出し後に1回だけ適用されます(コンストラクタとプロトタイプを変更します)


1.デコレータの計算



デコレータは、計算されたプロパティ名とともに式として評価されます。これは、左から右、上から下に発生します。デコレータの結果は、クラス定義が完了した後に呼び出される(使用される)一種のローカル変数に格納されます。



2.デコレータの呼び出し



デコレータは、ラップされた要素と、オプションでコンテキストオブジェクトの2つの引数で呼び出されます。



ラップされた要素:最初のパラメーター



デコレータがラップアラウンドする最初の議論は、私たちがデコレーションするものです(トートロジーについては申し訳ありません):



  • 単純なメソッド、初期化メソッド、ゲッターまたはセッターとなると、対応する関数
  • クラスについての場合:クラス自体
  • フィールドについての場合:2つのプロパティを持つオブジェクト:



    • get:レシーバーで呼び出されるパラメーターのない関数。レシーバーは、含まれている値を返すオブジェクトです。
    • セット:渡されたオブジェクトであるレシーバーで呼び出され、未定義を返す1つのパラメーター(新しい値)を受け取る関数


コンテキストオブジェクト:2番目のパラメーター



コンテキストオブジェクト(2番目の引数としてデコレータに渡されるオブジェクト)には、次のプロパティが含まれています。



  • 種類:次のいずれかの値になります。



    • "クラス"
    • "方法"
    • 「初期化メソッド」
    • 「ゲッター」
    • 「セッター」
    • "フィールド"
  • 名前:



    • パブリックフィールドまたはメソッド:名前-文字列または文字のプロパティキー
    • プライベートフィールドまたはメソッド:なし
    • クラス:欠席
  • isStatic:



    • 静的フィールドまたはメソッド:true
    • インスタンスフィールドまたはメソッド:false
    • クラス:欠席


「ターゲット」(「ターゲット」)は、デコレータが呼び出された時点でまだ構築されていないため、フィールドまたはメソッドのデコレータに渡されません。



戻り値



戻り値は、デコレータのタイプによって異なります。



  • クラス:新しいクラス
  • メソッド、ゲッターまたはセッター:新機能
  • フィールド:3つのプロパティを持つオブジェクト:



    • 取得する
    • セットする
    • initialize:setと同じ引数で呼び出され、変数の初期化に使用された値を返す関数。この関数は、基になるストレージの設定がフィールド初期化子またはメソッド定義に依存する場合に呼び出されます
  • initメソッド:2つのプロパティを持つオブジェクト:



    • メソッド:メソッドを置き換える関数
    • initialize:引数がなく、戻り値が無視され、新しく作成されたオブジェクトをレシーバーとして呼び出される関数。


3.デコレータの使用



デコレータは、呼び出された後に適用されます。デコレータの作業アルゴリズムの中間段階を修正することはできません。メソッドとインスタンスフィールドのすべてのデコレータが適用されるまで、新しく作成されたクラスにアクセスできません。



クラスデコレータは、フィールドデコレータとメソッドデコレータが適用された後に呼び出されます。



最後に、静的フィールドデコレータが適用されます。



フィールドデコレータのセマンティクス



クラスフィールドデコレータは、プライベートフィールドのゲッター/セッターペアです。したがって、コード:



function id(v) { return v }

class C {
  @id x = y
}

      
      





次のセマンティクスがあります。



class C {
  //  #    -
  #x = y
  get x() { return this.#x }
  set x(v) { this.#x = v }
}

      
      





フィールドデコレータはプライベートフィールドのように動作します。次のコードは、インスタンスに追加する前に「y」にアクセスしようとしているため、TypeError例外をスローします。



class C {
  @id x = this.y
  @id y
}
new C // TypeError

      
      





ゲッター/セッターのペアは通常のオブジェクトメソッドであり、他のメソッドと同様に列挙できません(必要に応じて列挙できません)。含まれるプライベートフィールドは、通常のプライベートフィールドと同様に、初期化子とともに1つずつ追加されます。



設計目標



  • 組み込みのデコレータは、独自に作成するのと同じくらい簡単に使用できるはずです。
  • デコレータは、副作用のない装飾されたオブジェクトにのみ適用する必要があります。


アプリケーションケース



  • クラスとメソッドへのメタデータの保存
  • フィールドをアクセサーに変換する
  • メソッドまたはクラスのラッピング(このデコレータの使用は、オブジェクトのプロキシにいくらか似ています)


の例



デコレータの実装と使用の例。



@logged



@loggedデコレータは、メソッド実行の開始と終了に関するメッセージをコンソールに出力します。@deprecatedのような関数をラップする他の人気のあるデコレータがあります。 デバウンス、@ memoizeなど。



使用:



//  .mjs   -
import { logged } from './logged.mjs'

class C {
  @logged
  m(arg) {
    this.#x = arg
  }

  @logged
  set #x(value) { }
}

new C().m(1)
//  m   1
//  set #x   1
//  set #x
//  m

      
      





@loggedは、デコレータとしてJavaScriptに実装できます。デコレータは、デコレートする要素を含む引数で呼び出される関数です。この要素は、メソッド、ゲッター、またはセッターにすることができます。デコレータは、2番目の引数であるコンテキストを使用して呼び出すことができますが、この場合は必要ありません。



デコレータによって返される値は、ラップされた要素を置き換えます。メソッド、ゲッター、およびセッターの場合、戻り値はそれらを置き換える関数です。



// logged.mjs

export function logged(f) {
  //   
  const name = f.name
  function wrapped(...args) {
    //    
    console.log(` ${name}   ${args.join(', ')}`)
    //    
    const ret = f.call(this, ...args)
    //    
    console.log(` ${name}`)
    //  
    return ret
  }
  // Object.defineProperty()       
  // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(wrapped, 'name', { value: name, configurable: true })
  //  
  return wrapped
}

      
      





与えられた例の変換の結果は次のようになります。



let x_setter

class C {
  m(arg) {
    this.#x = arg
  }

  static #x_setter(value) { }
  //  -     (class static initialization blocks)
  // https://github.com/tc39/proposal-class-static-block
  static { x_setter = C.#x_setter }
  set #x(value) { return x_setter.call(this, value) }
}

C.prototype.m = logged(C.prototype.m, { kind: "method", name: "m", isStatic: false })
x_setter = logged(x_setter, {kind: "setter", isStatic: false})

      
      





ゲッターとセッターは別々に装飾されていることに注意してください。アクセサ(計算されたプロパティ)は、前の節のように結合されません。



@defineElement



HTMLカスタム要素(カスタム要素、Webコンポーネントの一部)を使用すると、独自のHTML要素を作成できます。要素の登録は、customElements.defineを使用して行われ ます。デコレータを使用して要素を登録する方法は次のとおりです。



import { defineElement } from './defineElement.js'

@defineElement('my-class')
class MyClass extends HTMLElement { }

      
      





クラスは、メソッドやアクセサーと一緒に装飾することができます。



// defineElement.mjs
export function defineElement(name, options) {
  return klass => {
    customElements.define(name, klass, options); return klass
  }
}

      
      





デコレータは、それ自体が使用する引数を取るため、別の関数を返す関数として実装されます。あなたはそれを「デコレータファクトリー」と考えることができます:引数を渡した後、あなたは別のデコレータを手に入れます。



メタデータを追加するデコレータ



デコレータは、渡されたコンテキストオブジェクトにメタデータプロパティを追加することにより、クラスメンバーにメタデータを提供できます。メタデータを含むすべてのオブジェクトは、Object.assign使用して連結 され、[Symbol.metadata]クラスプロパティに配置されます。例えば:



//    
@annotate({x: 'y'}) @annotate({v: 'w'}) class C {
  //    
  @annotate({a: 'b'}) method() { }
  //    
  @annotate({c: 'd'}) field
}

C[Symbol.metadata].class.x                    // 'y'
C[Symbol.metadata].class.v                    // 'w'
// ,  ,    ,
C[Symbol.metadata].prototype.methods.method.a // 'b'
//   
C[Symbol.metadata].instance.fields.field.c    // 'd'

      
      





注釈付きオブジェクトの表示形式は概算であり、さらに洗練される可能性があることに注意してください。この例の主なタスクは、注釈がデータの読み取りまたは書き込みにライブラリを使用する必要のない単なるオブジェクトであり、システムによって自動的に作成されることを示すことです。



問題のデコレータは次のように実装できます。



function annotate(metadata) {
  return (_, context) => {
    context.metadata = metadata
    return _
  }
}

      
      





デコレータが呼び出されるたびに、新しいコンテキストがデコレータに渡され、未定義でない限り、メタデータプロパティが[Symbol.metadata]に含まれます。



メソッドではなくクラス自体に追加されたメタデータは、クラスで宣言されたデコレータでは使用できないことに注意してください。クラスへのメタデータの追加は、データの損失を回避するためにすべての「内部」デコレータを呼び出した後、コンストラクタで行われます。



@tracked



@trackedデコレータは、クラスフィールドを監視し、セッターが呼び出されたときにrenderメソッドを呼び出します。このパターンおよび同様のパターンは、再レンダリングの問題を解決するためにさまざまなフレームワークで広く使用されています。



装飾されたフィールドのセマンティクスは、いくつかのプライベートデータストアのゲッター/セッターラッパーを示唆しています。@trackedは、ゲッターとセッターのペアをラップして、再レンダリングロジックを実装できます。



import {tracked} from './tracked.mjs'

class Element {
  @tracked counter = 0

  increment() { this.counter++ }

  render() { console.log(counter) }
}

const e = new Element()
e.increment() //    1
e.increment() // 2

      
      





フィールドを装飾する場合、「ラップされた」値は、内部ストレージを管理するためのget関数とset関数の2つのプロパティを持つオブジェクトです。これらは、インスタンスに自動的にバインドするように設計されています(call()を使用)。



// tracked.mjs
export function tracked({ get, set }) {
  return {
    get,
    set(value) {
      if (get.call(this) !== value) {
        set.call(this, value)
        this.render()
      }
    }
  }
}

      
      





プライベートフィールドとメソッドへのアクセスが制限されています



クラス外の一部のコードは、プライベートフィールドまたはメソッドにアクセスする必要がある場合があります。たとえば、2つのクラス間の相互運用性を提供したり、クラス内のコードをテストしたりします。



デコレータを使用すると、プライベートフィールドとメソッドにアクセスできます。このロジックは、必要に応じて提供される秘密参照キーを使用してオブジェクトにカプセル化できます。



import { PrivateKey } from './private-key.mjs'

let key = new PrivateKey()

export class Box {
  @key.show #contents
}

export function setBox(box, contents) {
  return key.set(box, contents)
}

export function getBox(box) {
  return key.get(box)
}

      
      





上記の例は、private.nameでプライベート名を参照したり、private / withでプライベート名の範囲を拡張したりする などの構造を使用すると、実装が簡単な一種のハックであることに注意してください ただし、この提案が既存の機能を有機的に拡張する方法を示しています。



// private-key.mjs
export class PrivateKey {
#get
#set

show({ get, set }) {
  assert(this.#get === undefined && this.#set === undefined)
  this.#get = get
  this.#set = set
  return { get, set }
}
get(obj) {
  return this.#get.call(obj)
}
set(obj, value) {
  return this.#set.call(obj, value)
}
}

      
      





@deprecated



@deprecatedデコレータは、非推奨のフィールド、メソッド、またはアクセサの使用に関する警告をコンソールに出力します。使用例:



import { deprecated } from './deprecated.mjs'

export class MyClass {
  @deprecated field

  @deprecated method() { }

  otherMethod() { }
}

      
      





デコレータがクラスのさまざまな要素を処理できるようにするために、コンテキストのkindフィールドは、廃止として認識された構文構造のタイプをデコレータに通知します。この手法では、デコレータが無効なコンテキストで使用されている場合に例外をスローすることもできます。たとえば、内部クラスはアクセスを拒否できないため、非推奨としてマークすることはできません。



function wrapDeprecated(fn) {
  let name = fn.name
  function method(...args) {
    console.warn(` ${name}  `)
    return fn.call(this, ...args)
  }
  Object.defineProperty(method, 'name', { value: name, configurable: true })
  return method
}

export function deprecated(element, { kind }) {
  switch (kind) {
    case 'method':
    case 'getter':
    case 'setter':
      return wrapDeprecated(element)
    case 'field': {
      let { get, set } = element
      return { get: wrapDeprecated(get), set: wrapDeprecated(set) }
    }
    default:
      //  'class'
      throw new Error(`${kind}    @deprecated`)
  }
}

      
      





事前設定が必要なメソッドデコレータ



一部のメソッドデコレータは、クラスがインスタンス化されるときにコードの実行に依存しています。例えば:



  • クラスメソッドの@on( 'event')デコレータはHTMLElementを拡張し、このメソッドをコンストラクタのイベントハンドラとして登録します。
  • @boundデコレータは、コンストラクタのthis.method = this.method.bind(this)と同等です。


名前付きデコレータを使用するには、さまざまな方法があります。



オプション1:コンストラクターとメタデータ



これらのデコレータは、コンストラクタで使用される初期化操作を含むメタデータとミックスインの組み合わせです。



タッチで@on



class MyClass extends WithActions(HTMLElement) {
  @on('click') clickHandler() {}
}

      
      





指定されたデコレータは次のように定義できます。



//         ,
//   Symbol
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Symbol
const handler = Symbol('handler')
function on(eventName) {
  return (method, context) => {
    context.metadata = { [handler]: eventName }
    return method
  }
}

class MetadataLookupCache {
  //     ,
  //      WeakMap
  // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
  #map = new WeakMap()
  #name
  constructor(name) { this.#name = name }
  get(newTarget) {
    let data = this.#map.get(newTarget)
    if (data === undefined) {
      data = []
      let klass = newTarget
      while (klass !== null && !(this.#name in klass)) {
        for (const [name, { [this.#name]: eventName }] of Object.entries(klass[Symbol.metadata].instance.methods)) {
          if (eventName !== undefined) {
            data.push({ name, eventName })
          }
        }
        klass = klass.__proto__
      }
      this.#map.set(newTarget, data)
    }
    return data
  }
}

const handlersMap = new MetadataLookupCache(handler)

function WithActions(superClass) {
  return class C extends superClass {
    constructor(...args) {
      super(...args)
      const handlers = handlersMap.get(new.target, C)
      for (const { name, eventName } of handlers) {
        this.addEventListener(eventName, this[name].bind(this))
      }
    }
  }
}

      
      





@バウンドとミックスイン



@boundは次のように使用できます。



class C extends WithBoundMethod(Object) {
  #x = 1
  @bound method() { return this.#x }
}

const c = new C()
const m = c.method
m() // 1,   TypeError

      
      





デコレータの実装は次のようになります。



const boundName = Symbol('boundName')
function bound(method, context) {
  context.metadata = { [boundName]: true }
  return method
}

const boundMap = new MetadataLookupCache(boundName)

function WithBoundMethods(superClass) {
  return class C extends superClass {
    constructor(...args) {
      super(...args)
      const names = boundMap.get(new.target, C)
      for (const { name } of names) {
        this[name] = this[name].bind(this)
      }
    }
  }
}

      
      





両方の例でMetadataLookupCacheが使用されていることに注意してください。また、この文と次の文は、メタデータを追加するために何らかの標準ライブラリを使用することを前提としていることに注意してください。



オプション2:メソッドデコレータ 初期化



デコレータ 初期化:初期化操作が必要であるが、スーパークラス/ミックスインを呼び出すことができない場合を対象としています。これにより、コンストラクターの実行時にそのような操作を追加できます。



@on c init



使用:



class MyElement extends HTMLElement {
  @init: on('click') clickHandler()
}

      
      





デコレータ 初期化:はメソッドデコレータと同じように呼び出されますが、{method、initialize}ペアを返します。ここで、initializeは、この値として新しいインスタンスで呼び出され、引数はなく、何も返しません。



function on(eventName) {
  return (method, context) => {
    assert(context.kind === 'init-method')
    return { method, initialize() { this.addEventListener(eventName, method) } }
  }
}

      
      





@bound with init



初期化:デコレータの作成にも使用できます 初期化: バウンド:



class C {
  #x = 1
  @init: bound method() { return this.#x }
}

const c = new C()
const m = c.method
m() // 1,   TypeError

      
      





@boundデコレータは、次のように実装できます。



function bound(method, { kind, name }) {
  assert(kind === 'init-method')
  return { method, initialize() { this[name] = this[name].bind(this) } }
}

      
      





使用の制限の詳細、およびJavaScriptでデコレータを標準化する前に開発者が解決しなければならない未解決の質問については、記事の冒頭にあるリンクにある提案のテキストを参照してください。



これで、私は私の休暇を取りましょう。清聴ありがとうございました。



All Articles