Selamat siang teman!
Saya sampaikan kepada Anda terjemahan yang diadaptasi dari proposal baru (September 2020) mengenai penggunaan dekorator di JavaScript, dengan sedikit penjelasan tentang apa yang terjadi.
Proposal ini pertama kali dibuat sekitar 5 tahun yang lalu dan telah mengalami beberapa perubahan signifikan sejak saat itu. Saat ini (masih) dalam tahap pertimbangan kedua.
Jika Anda belum pernah mendengar tentang dekorator sebelumnya atau ingin memoles pengetahuan Anda, saya sarankan Anda membaca artikel berikut:
Jadi apakah dekorator itu? Dekorator adalah fungsi yang dipanggil pada elemen kelas (bidang atau metode) atau pada kelas itu sendiri selama definisinya, yang membungkus atau mengganti elemen (atau kelas) dengan nilai baru (dikembalikan oleh dekorator).
Bidang kelas yang didekorasi diperlakukan sebagai pembungkus dari pengambil / penyetel, memungkinkan Anda untuk mengambil / menetapkan (mengubah) nilai ke bidang itu.
Dekorator juga dapat membuat anotasi elemen kelas dengan metadata. Metadata adalah kumpulan properti objek sederhana yang ditambahkan oleh dekorator. Mereka tersedia sebagai satu set objek bertingkat di properti [Symbol.metadata].
Sintaksis
Sintaks penghias, selain awalan @ (@decoratorName), mengasumsikan berikut ini:
- Ekspresi dekorator terbatas pada rangkaian variabel (beberapa dekorator dapat digunakan), mengakses properti dengan., Tapi tidak dengan [], dan memanggil dengan ()
- Tidak hanya definisi kelas yang dapat didekorasi, tetapi juga elemennya (bidang dan metode)
- Dekorator kelas ditentukan setelah ekspor dan default
Tidak ada aturan khusus untuk mendefinisikan dekorator; fungsi apa pun dapat digunakan seperti itu.
Detail semantik
Dekorator dievaluasi dalam tiga langkah:
- Ekspresi dekorator (apa pun setelah @) dievaluasi bersama dengan nama properti yang dihitung
- Dekorator dipanggil (sebagai fungsi) selama definisi kelas, setelah mengevaluasi metode, tetapi sebelum menggabungkan konstruktor dan prototipe
- Dekorator diterapkan (mengubah konstruktor dan prototipe) hanya sekali setelah panggilan
1. Menghitung dekorator
Dekorator dievaluasi sebagai ekspresi bersama dengan nama properti yang dihitung. Ini terjadi dari kiri ke kanan dan dari atas ke bawah. Hasil dari dekorator disimpan dalam semacam variabel lokal yang dipanggil (digunakan) setelah definisi kelas selesai.
2. Memanggil dekorator
Dekorator dipanggil dengan dua argumen: elemen yang dibungkus dan, secara opsional, objek konteks.
Elemen yang dibungkus: parameter pertama
Argumen pertama yang dibungkus dekorator adalah apa yang kita hias (maaf untuk tautologi):
- Ketika datang ke metode sederhana, metode inisialisasi, pengambil atau penyetel: fungsi yang sesuai
- Jika tentang kelas: kelas itu sendiri
- If about field: sebuah objek dengan dua properti:
- get: fungsi tanpa parameter yang dipanggil dengan penerima, yang merupakan objek yang mengembalikan nilai yang dikandungnya
- set: fungsi yang mengambil satu parameter (nilai baru), yang dipanggil dengan penerima yang merupakan objek yang diteruskan, dan mengembalikan tidak terdefinisi
Objek konteks: parameter kedua
Objek konteks - objek yang diteruskan ke dekorator sebagai argumen kedua - berisi properti berikut:
- kind: memiliki salah satu nilai berikut:
- "Kelas"
- "Metode"
- "Metode-Init"
- "Getter"
- "Setter"
- "Bidang"
- nama:
- public field atau metode: nama - string atau kunci properti karakter
- bidang atau metode pribadi: tidak ada
- class: absen
- isStatic:
- bidang atau metode statis: benar
- contoh bidang atau metode: salah
- class: absen
"Target" (konstruktor atau prototipe) tidak diteruskan ke bidang atau dekorator metode karena itu ("target") belum dibuat pada saat dekorator dipanggil.
Nilai kembali
Nilai pengembalian tergantung pada jenis dekorator:
- kelas: kelas baru
- metode, pengambil atau penyetel: fungsi baru
- field: sebuah objek dengan tiga properti:
- Dapatkan
- set
- inisialisasi: fungsi yang dipanggil dengan argumen yang sama seperti set, mengembalikan nilai yang digunakan untuk menginisialisasi variabel. Fungsi ini dipanggil jika pengaturan penyimpanan yang mendasari bergantung pada penginisialisasi bidang atau definisi metode
- metode init: sebuah objek dengan dua properti:
- metode: fungsi yang menggantikan metode
- inisialisasi: fungsi tanpa argumen, yang nilai kembaliannya diabaikan, dan yang dipanggil dengan objek yang baru dibuat sebagai penerima
3. Menggunakan dekorator
Dekorator diterapkan setelah mereka dipanggil. Tahap perantara dari algoritme kerja dekorator tidak dapat diperbaiki - kelas yang baru dibuat tidak dapat diakses hingga semua dekorator metode dan bidang contoh diterapkan.
Dekorator kelas dipanggil setelah dekorator lapangan dan metode diterapkan.
Akhirnya, dekorator bidang statis diterapkan.
Semantik dekorator lapangan
Dekorator bidang kelas adalah pasangan pengambil / penyetel untuk bidang pribadi. Oleh karena itu kodenya:
function id(v) { return v }
class C {
@id x = y
}
memiliki semantik berikut:
class C {
// # -
#x = y
get x() { return this.#x }
set x(v) { this.#x = v }
}
Dekorator lapangan berperilaku seperti bidang pribadi. Kode berikut akan memunculkan pengecualian TypeError karena kita mencoba mengakses "y" sebelum menambahkannya ke instance:
class C {
@id x = this.y
@id y
}
new C // TypeError
Pasangan pengambil / penyetel adalah metode biasa pada suatu objek, yang tidak dapat dihitung (tidak dapat dihitung, jika Anda mau) seperti metode lain. Bidang pribadi yang dikandungnya ditambahkan satu per satu, bersama dengan penginisialisasi, seperti bidang pribadi biasa.
Tujuan desain
- Harus semudah menggunakan dekorator built-in seperti halnya menulis sendiri
- Dekorator sebaiknya hanya diterapkan pada objek dekorasi tanpa efek samping.
Kasus aplikasi
- Menyimpan metadata di kelas dan metode
- Mengonversi bidang menjadi pengakses
- Membungkus metode atau kelas (penggunaan dekorator ini agak mirip dengan proxy objek)
Contoh dari
Contoh penerapan dan penggunaan dekorator.
@logged
Dekorator @logged mencetak pesan ke konsol tentang awal dan akhir eksekusi metode. Ada dekorator populer lainnya yang membungkus fungsi seperti: @deprecated. debounce, @memoize, dll.
Menggunakan:
// .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 dapat diimplementasikan dalam JavaScript sebagai dekorator. Dekorator adalah fungsi yang dipanggil dengan argumen yang berisi elemen dekorasi. Elemen ini bisa berupa metode, pengambil, atau penyetel. Dekorator dapat dipanggil dengan argumen kedua, dalam konteksnya, dalam hal ini kita tidak membutuhkannya.
Nilai yang dikembalikan oleh dekorator menggantikan elemen yang dibungkus. Untuk metode, pengambil, dan penyetel, nilai yang dikembalikan adalah fungsi yang menggantikannya.
// 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
}
Hasil transpilasi dari contoh yang diberikan mungkin terlihat seperti ini:
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})
Perhatikan bahwa pengambil dan penyetel didekorasi secara terpisah. Accessor (properti yang dihitung) tidak digabungkan seperti pada klausa sebelumnya.
@definelement
Elemen Kustom HTML (elemen kustom, bagian dari komponen web) memungkinkan Anda membuat elemen HTML Anda sendiri. Pendaftaran elemen dilakukan menggunakan customElements.define . Berikut cara mendaftarkan elemen menggunakan dekorator:
import { defineElement } from './defineElement.js'
@defineElement('my-class')
class MyClass extends HTMLElement { }
Kelas dapat didekorasi bersama dengan metode dan pengakses.
// defineElement.mjs
export function defineElement(name, options) {
return klass => {
customElements.define(name, klass, options); return klass
}
}
Dekorator mengambil argumen yang digunakannya sendiri, sehingga diimplementasikan sebagai fungsi yang mengembalikan fungsi lain. Anda dapat menganggap ini sebagai "pabrik dekorator": setelah melewati argumen, Anda mendapatkan dekorator yang berbeda.
Dekorator menambahkan metadata
Dekorator dapat memberikan metadata kepada anggota kelas dengan menambahkan properti metadata ke objek konteks yang diteruskan kepada mereka. Semua objek yang berisi metadata digabungkan menggunakan Object.assign dan ditempatkan di properti kelas [Symbol.metadata]. Sebagai contoh:
//
@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'
Harap diperhatikan bahwa format presentasi dari objek yang dianotasi adalah perkiraan dan dapat disempurnakan lebih lanjut. Tugas utama dari contoh ini adalah untuk menunjukkan bahwa anotasi hanyalah sebuah objek yang tidak memerlukan penggunaan perpustakaan untuk membaca atau menulis datanya; anotasi dibuat oleh sistem secara otomatis.
Dekorator yang dimaksud dapat diimplementasikan seperti ini:
function annotate(metadata) {
return (_, context) => {
context.metadata = metadata
return _
}
}
Setiap kali dekorator dipanggil, konteks baru diteruskan padanya, kemudian properti metadata, asalkan tidak ditentukan, disertakan dalam [Symbol.metadata].
Perhatikan bahwa metadata yang ditambahkan ke kelas itu sendiri, dan bukan ke metodenya, tidak tersedia untuk dekorator yang dideklarasikan di kelas tersebut. Penambahan metadata ke kelas terjadi di konstruktor setelah memanggil semua dekorator "internal" untuk menghindari kehilangan data.
@ed
Dekorator @tracked mengamati bidang kelas dan memanggil metode render saat penyetel dipanggil. Pola ini dan pola serupa banyak digunakan oleh berbagai kerangka kerja untuk memecahkan masalah rendering ulang.
Semantik bidang yang didekorasi menyarankan pembungkus pengambil / penyetel di sekitar penyimpanan data pribadi. @tracked dapat membungkus pasangan pengambil / penyetel untuk mengimplementasikan logika rendering ulang:
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
Saat mendekorasi bidang, nilai "dibungkus" adalah objek dengan dua properti: fungsi get dan setel untuk mengelola penyimpanan internal. Mereka dirancang untuk secara otomatis mengikat ke sebuah instance (menggunakan call ()).
// tracked.mjs
export function tracked({ get, set }) {
return {
get,
set(value) {
if (get.call(this) !== value) {
set.call(this, value)
this.render()
}
}
}
}
Akses terbatas ke bidang dan metode pribadi
Terkadang, beberapa kode di luar kelas mungkin perlu mengakses bidang atau metode pribadi. Misalnya, untuk menyediakan interoperabilitas antara dua kelas atau untuk menguji kode dalam sebuah kelas.
Dekorator memungkinkan untuk mengakses bidang dan metode pribadi. Logika ini dapat dikemas dalam sebuah objek dengan kunci referensi pribadi yang disediakan sesuai kebutuhan.
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)
}
Perhatikan bahwa contoh di atas adalah jenis peretasan yang lebih mudah diterapkan dengan konstruksi seperti merujuk nama pribadi dengan private.name atau memperluas cakupan nama pribadi dengan private / with . Namun, ini menunjukkan bagaimana proposal ini secara organik memperluas fungsionalitas yang ada.
// 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)
}
}
@tokopedia
Dekorator @deprecated mencetak peringatan ke konsol tentang penggunaan kolom, metode, atau aksesor yang tidak digunakan lagi. Contoh penggunaan:
import { deprecated } from './deprecated.mjs'
export class MyClass {
@deprecated field
@deprecated method() { }
otherMethod() { }
}
Untuk memungkinkan dekorator bekerja dengan elemen kelas yang berbeda, bidang konteks jenis memberi tahu dekorator tentang jenis konstruksi sintaksis yang dikenali sebagai usang. Teknik ini juga memungkinkan Anda untuk melempar pengecualian ketika dekorator digunakan dalam konteks yang tidak valid, misalnya: kelas dalam tidak dapat ditandai sebagai usang karena aksesnya tidak dapat ditolak.
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`)
}
}
Metode Dekorator Membutuhkan Prekonfigurasi
Beberapa dekorator metode mengandalkan mengeksekusi kode saat membuat instance kelas. Sebagai contoh:
- Dekorator @on ('event') untuk metode kelas memperluas HTMLElement, yang mendaftarkan metode ini sebagai pengendali kejadian di konstruktor
- Dekorator @bound setara dengan this.method = this.method.bind (this) di konstruktor
Ada berbagai cara untuk menggunakan dekorator bernama.
Opsi 1: konstruktor dan metadata
Dekorator ini adalah kombinasi metadata dan mixin yang berisi operasi inisialisasi yang digunakan dalam konstruktor.
@on dengan satu sentuhan
class MyClass extends WithActions(HTMLElement) {
@on('click') clickHandler() {}
}
Dekorator yang ditentukan dapat didefinisikan seperti ini:
// ,
// 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))
}
}
}
}
@ terikat dengan mixin
@bound dapat digunakan seperti ini:
class C extends WithBoundMethod(Object) {
#x = 1
@bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
Penerapan dekorator mungkin terlihat seperti ini:
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)
}
}
}
}
Perhatikan bahwa MetadataLookupCache digunakan di kedua contoh. Juga, perlu diingat bahwa ini dan kalimat berikut mengasumsikan penggunaan semacam pustaka standar untuk menambahkan metadata.
Opsi 2: dekorator metode init
Penghias init: ditujukan untuk kasus di mana operasi inisialisasi diperlukan, tetapi tidak mungkin untuk memanggil superclass / mixin. Ini memungkinkan Anda untuk menambahkan operasi seperti itu ketika konstruktor dijalankan.
@ di c init
Menggunakan:
class MyElement extends HTMLElement {
@init: on('click') clickHandler()
}
Penghias init: Dipanggil seperti dekorator metode, tetapi mengembalikan pasangan {method, initialize}, di mana inisialisasi dipanggil dengan instance baru sebagai nilai ini, tanpa argumen, dan tidak mengembalikan apa-apa.
function on(eventName) {
return (method, context) => {
assert(context.kind === 'init-method')
return { method, initialize() { this.addEventListener(eventName, method) } }
}
}
@terikat dengan init
init: juga dapat digunakan untuk membangun dekorator init: terikat:
class C {
#x = 1
@init: bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
Dekorator @bound dapat diimplementasikan seperti ini:
function bound(method, { kind, name }) {
assert(kind === 'init-method')
return { method, initialize() { this[name] = this[name].bind(this) } }
}
Untuk informasi lebih lanjut tentang batasan penggunaan, serta pertanyaan terbuka yang harus diselesaikan pengembang sebelum menstandarisasi dekorator di JavaScript, lihat teks proposal di tautan yang disediakan di awal artikel.
Dalam hal ini, biarkan aku pergi. Terima kasih atas perhatian Anda.