Metaprogramming adalah jenis pemrograman yang terkait dengan pembuatan program yang menghasilkan program lain sebagai hasil dari pekerjaan mereka, atau program yang mengubah diri mereka sendiri selama eksekusi. (Wikipedia)
Dalam bahasa yang lebih sederhana, metaprogramming dalam JavaScript dapat dianggap sebagai mekanisme yang memungkinkan Anda untuk menganalisis dan mengubah program secara real time tergantung pada tindakan apa pun. Dan, kemungkinan besar, Anda entah bagaimana menggunakannya saat menulis skrip setiap hari.
JavaScript, pada dasarnya, adalah bahasa dinamis yang sangat kuat dan memungkinkan Anda untuk menulis kode fleksibel dengan baik:
const comment = { authorId: 1, comment: '' }; for (let name in comment) { const pascalCasedName = name.slice(0, 1).toUpperCase() + name.slice(1); comment[`save${pascalCasedName}`] = function() {
Kode serupa untuk secara dinamis membuat metode dalam bahasa lain mungkin memerlukan sintaks atau API khusus untuk ini. Sebagai contoh, PHP juga merupakan bahasa yang dinamis, tetapi di dalamnya akan membutuhkan lebih banyak usaha:
<?php class Comment { public $authorId; public $comment; public function __construct($authorId, $comment) { $this->authorId = $authorId; $this->comment = $comment; }
Selain sintaks fleksibel, kami juga memiliki banyak fungsi yang berguna untuk menulis kode dinamis: Object.create, Object.defineProperty, Function.apply dan banyak lainnya.
Pertimbangkan mereka secara lebih rinci.
- Pembuatan kode
- Bekerja dengan fungsi
- Bekerja dengan benda
- Refleksikan API
- Simbol
- Proksi
- Kesimpulan
1. Pembuatan Kode
Alat standar untuk mengeksekusi kode secara dinamis adalah fungsi eval , yang memungkinkan Anda untuk mengeksekusi kode dari string yang diteruskan:
eval('alert("Hello, world")');
Sayangnya, eval memiliki banyak nuansa:
- jika kode kita ditulis dalam mode ketat ('gunakan ketat'), maka variabel yang dideklarasikan di dalam eval tidak akan terlihat dalam kode panggilan eval. Pada saat yang sama, kode itu sendiri di dalam eval selalu dapat mengubah variabel eksternal.
- kode di dalam eval dapat dieksekusi baik dalam konteks global (jika dipanggil melalui window.eval) dan dalam konteks fungsi di mana panggilan dilakukan (jika hanya eval, tanpa jendela).
- masalah dapat muncul karena JS minification, ketika nama variabel diganti dengan yang lebih pendek untuk mengurangi ukuran. Kode yang diteruskan sebagai string untuk eval biasanya tidak menyentuh minifier, karena ini kita dapat mulai mengakses variabel eksternal menggunakan nama lama yang tidak diubah, yang akan mengarah pada kesalahan halus.
Ada alternatif yang bagus untuk menyelesaikan masalah ini - Fungsi baru .
const hello = new Function('name', 'alert("Hello, " + name)'); hello('')
Tidak seperti eval, kita selalu dapat secara eksplisit melewati parameter melalui argumen fungsi dan secara dinamis memberikan konteksnya (melalui Function.apply atau Function.call ). Selain itu, fungsi yang dibuat selalu disebut dalam lingkup global.
Di masa lalu, eval sering digunakan untuk mengubah kode secara dinamis, seperti JavaScript memiliki sedikit mekanisme untuk refleksi dan tidak mungkin dilakukan tanpa eval. Tetapi dalam standar bahasa modern, fungsionalitas tingkat tinggi jauh lebih muncul dan eval sekarang lebih jarang digunakan.
2. Bekerja dengan fungsi
JavaScript memberi kita banyak alat luar biasa untuk bekerja secara dinamis dengan berbagai fungsi, memungkinkan kita untuk mendapatkan berbagai informasi tentang fungsi dalam runtime dan mengubahnya:
Function.length - memungkinkan Anda untuk mengetahui jumlah argumen dari suatu fungsi:
const func = function(name, surname) { console.log(`Hello, ${surname} ${name}`) }; console.log(func.length)
Function.apply dan Function.call - memungkinkan Anda untuk mengubah konteks fungsi ini secara dinamis:
const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce();
Mereka berbeda satu sama lain hanya dalam hal itu, di Function.apply, argumen ke fungsi disajikan sebagai array, dan di Function.call, dipisahkan oleh koma. Fitur ini sering digunakan sebelumnya untuk meneruskan daftar argumen ke fungsi sebagai array. Contoh umum adalah fungsi Math.max (secara default, tidak dapat bekerja dengan array):
Math.max.apply(null, [1, 2, 4, 3]);
Dengan munculnya operator spread baru , Anda cukup menulis ini:
Math.max(...[1, 2, 4, 3]);
Function.bind - memungkinkan Anda membuat salinan fungsi dari yang sudah ada, tetapi dengan konteks yang berbeda:
const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce();
Function.caller - memungkinkan Anda mendapatkan fungsi panggilan. Tidak disarankan untuk menggunakannya , karena tidak ada dalam standar bahasa dan tidak akan bekerja dalam mode ketat. Ini disebabkan oleh fakta bahwa jika berbagai mesin JavaScript menerapkan pengoptimalan panggilan ekor yang dijelaskan dalam spesifikasi bahasa, maka memanggil Function.caller mungkin mulai menghasilkan hasil yang salah. Contoh penggunaan:
const a = function() { console.log(a.caller == b); } const b = function() { a(); } b();
Function.toString - Mengembalikan representasi string dari fungsi. Ini adalah fitur yang sangat kuat yang memungkinkan Anda memeriksa isi suatu fungsi dan argumennya:
const getFullName = (name, surname, middlename) => { console.log(`${surname} ${name} ${middlename}`); } getFullName.toString()
Setelah menerima representasi string dari suatu fungsi, kita dapat menguraikan dan menguraikannya. Ini dapat digunakan untuk, misalnya, mengeluarkan nama argumen fungsi dan, tergantung pada namanya, secara otomatis mengganti parameter yang diinginkan. Secara umum, ada dua cara untuk menguraikan:
- Parsing sekelompok pelanggan tetap dan kami mendapatkan tingkat keandalan yang dapat diterima (mungkin tidak berfungsi jika kami tidak mencakup semua jenis entri fungsi yang mungkin).
- Kami mendapatkan representasi string dari fungsi dan memasukkannya ke parser JavaScript yang sudah jadi (misalnya, esprima atau biji ), dan kemudian kami bekerja dengan AST terstruktur. Contoh parsing AST melalui esprima. Saya juga dapat menyarankan laporan yang bagus tentang parser dari Alexei Okhrimenko.
Contoh sederhana dengan parsing fungsi reguler:
Mendapatkan daftar argumen fungsi const getFunctionParams = fn => { const COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/gm; const DEFAULT_PARAMS = /=[^,]+/gm; const FAT_ARROW = /=>.*$/gm; const ARGUMENT_NAMES = /([^\s,]+)/g; const formattedFn = fn .toString() .replace(COMMENTS, "") .replace(FAT_ARROW, "") .replace(DEFAULT_PARAMS, ""); const params = formattedFn .slice(formattedFn.indexOf("(") + 1, formattedFn.indexOf(")")) .match(ARGUMENT_NAMES); return params || []; }; const getFullName = (name, surname, middlename) => { console.log(surname + ' ' + name + ' ' + middlename); }; console.log(getFunctionParams(getFullName));
Mendapatkan fungsi tubuh const getFunctionBody = fn => { const restoreIndent = body => { const lines = body.split("\n"); const bodyLine = lines.find(line => line.trim() !== ""); let indent = typeof bodyLine !== "undefined" ? (/[ \t]*/.exec(bodyLine) || [])[0] : ""; indent = indent || ""; return lines.map(line => line.replace(indent, "")).join("\n"); }; const fnStr = fn.toString(); const rawBody = fnStr.substring( fnStr.indexOf("{") + 1, fnStr.lastIndexOf("}") ); const indentedBody = restoreIndent(rawBody); const trimmedBody = indentedBody.replace(/^\s+|\s+$/g, ""); return trimmedBody; }; // getFullName const getFullName = (name, surname, middlename) => { console.log(surname + ' ' + name + ' ' + middlename); }; console.log(getFunctionBody(getFullName));
Penting untuk dicatat bahwa saat menggunakan minifier, kode itu sendiri di dalam fungsi parsed dan argumennya dapat dioptimalkan dan, karenanya, berubah.
3. Bekerja dengan benda
JavaScript memiliki objek Objek global yang berisi banyak metode untuk bekerja secara dinamis dengan objek.
Sebagian besar metode dari sana sudah lama ada dalam bahasa dan digunakan secara luas.
Properti Obyek
Object.assign - untuk dengan mudah menyalin properti dari satu atau lebih objek ke objek yang ditentukan oleh parameter pertama:
Object.assign({}, { a: 1 }, { b: 2 }, { c: 3 })
Object.keys dan Object.values - mengembalikan daftar kunci atau daftar nilai objek:
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.keys(obj));
Object.entries - mengembalikan daftar propertinya dalam format [[key1, value1], [key2, value2]] :
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.entries(obj));
Object.prototype.hasOwnProperty - Memeriksa apakah properti terkandung dalam suatu objek (tidak dalam rantai prototipe):
const obj = { a: 1 }; obj.__proto__ = { b: 2 }; console.log(obj.hasOwnProperty('a'));
Object.getOwnPropertyNames - mengembalikan daftar propertinya sendiri, termasuk enumerasi dan non-enumerasi:
const obj = { a: 1, b: 2 }; Object.defineProperty(obj, 'c', { value: 3, enumerable: false });
Object.getOwnPropertySymbols - mengembalikan daftar karakternya sendiri (terkandung dalam objek, dan bukan dalam rantai prototipe):
const obj = {}; const a = Symbol('a'); obj[a] = 1; console.log(Object.getOwnPropertySymbols(obj));
Object.prototype.propertyIsEnumerable - memeriksa apakah properti enumerable (misalnya, tersedia di for-in, for-of loop):
const arr = [ ' ' ]; console.log(arr.propertyIsEnumerable(0));
Penjelas Properti Obyek
Deskriptor memungkinkan Anda menyempurnakan parameter properti. Dengan menggunakannya, kita dapat dengan mudah membuat pencegat kita sendiri saat membaca / menulis properti apa pun (getter dan setter - get / set), membuat properti tidak berubah atau tidak dapat dihitung, dan sejumlah hal lainnya.
Object.defineProperty dan Object.defineProperties - membuat satu atau lebih deskriptor properti. Buat deskriptor Anda sendiri dengan pengambil dan penyetel:
const obj = { name: '', surname: '' }; Object.defineProperty(obj, 'fullname', {
Pada contoh di atas, properti nama lengkap tidak memiliki nilai sendiri, tetapi secara dinamis bekerja dengan properti nama dan nama keluarga. Tidak perlu mendefinisikan pengambil dan penyetel sekaligus - kita hanya dapat meninggalkan pengambil dan mendapatkan properti read-only. Atau kita dapat menambahkan tindakan tambahan di setter bersama dengan pengaturan nilai, misalnya, logging.
Selain properti get / set, deskriptor memiliki beberapa properti untuk dikonfigurasi:
const obj = {};
Object.getOwnPropertyDescriptor dan Object.getOwnPropertyDescriptors - memungkinkan Anda untuk mendapatkan deskriptor objek yang diinginkan atau daftar lengkapnya:
const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyDescriptor(obj, "a"));
Membuat batasan saat bekerja dengan objek
Object.freeze - "membekukan" properti suatu objek. Konsekuensi dari "pembekuan" tersebut adalah kekekalan penuh dari properti objek - mereka tidak dapat diubah dan dihapus, yang baru ditambahkan, deskriptor diubah:
const obj = Object.freeze({ a: 1 });
Object.seal - "menyegel" properti suatu objek. Sealing mirip dengan Object.freeze, tetapi memiliki sejumlah perbedaan. Kami, seperti dalam Object.freeze, melarang menambahkan properti baru, menghapus yang sudah ada, mengubah deskriptornya, tetapi pada saat yang sama kami dapat mengubah nilai properti:
const obj = Object.seal({ a: 1 }); obj.a = 2;
Object.preventExtensions - melarang menambahkan properti / deskriptor baru:
const obj = Object.preventExtensions({ a: 1 }); obj.a = 2;
Prototipe objek
Object.create - untuk membuat objek dengan prototipe yang ditentukan dalam parameter. Fitur ini dapat digunakan untuk pewarisan prototipe dan untuk membuat objek "bersih", tanpa properti dari Object.prototype :
const pureObj = Object.create(null);
Object.getPrototypeOf dan Object.setPrototypeOf - untuk mendapatkan / mengubah prototipe objek:
const duck = {}; const bird = {}; Object.setPrototypeOf(duck, bird); console.log(Object.getPrototypeOf(duck) === bird);
Object.prototype.isPrototypeOf - Memeriksa apakah objek saat ini terkandung dalam rantai prototipe yang lain:
const duck = {}; const bird = {}; duck.__proto__ = bird; console.log(bird.isPrototypeOf(duck));
4. Refleksikan API
Dengan munculnya ES6, objek Reflect global telah ditambahkan ke JavaScript untuk menyimpan berbagai metode yang berkaitan dengan refleksi dan introspeksi.
Sebagian besar metodenya adalah hasil dari mentransfer metode yang ada dari objek global seperti Object dan Function ke namespace terpisah dengan sedikit refactoring untuk penggunaan yang lebih nyaman.
Mentransfer fungsi ke objek Reflect tidak hanya memfasilitasi pencarian metode yang diperlukan untuk refleksi dan memberikan semantik yang lebih besar, tetapi juga menghindari situasi yang tidak menyenangkan ketika objek kita tidak mengandung Object.prototype dalam prototipe, tetapi kami ingin menggunakan metode dari sana:
let obj = Object.create(null); obj.qwerty = 'qwerty'; console.log(obj.__proto__)
Refactoring telah membuat perilaku metode lebih eksplisit dan monoton. Sebagai contoh, jika sebelumnya, ketika memanggil Object.defineProperty pada nilai yang salah (seperti angka atau string), sebuah pengecualian dilemparkan, tetapi pada saat yang sama, memanggil Object.getOwnPropertyDescriptor pada deskriptor objek yang tidak ada secara diam-diam kembali tanpa terdefinisi, maka metode serupa dari Reflect selalu melempar pengecualian untuk data yang salah .
Beberapa metode baru juga telah ditambahkan:
Reflect.construct adalah alternatif yang lebih nyaman untuk Object.create , yang memungkinkan tidak hanya untuk membuat objek dengan prototipe yang ditentukan, tetapi juga untuk segera menginisialisasi:
function Person(name, surname) { this.name = this.formatParam(name); this.surname = this.formatParam(surname); } Person.prototype.formatParam = function(param) { return param.slice(0, 1).toUpperCase() + param.slice(1).toLowerCase(); } const oldPerson = Object.create(Person.prototype);
Reflect.ownKeys - mengembalikan array properti milik objek yang ditentukan (dan bukan ke objek dalam rantai prototipe):
let person = { name: '', surname: '' }; person.__proto__ = { age: 30 }; console.log(Reflect.ownKeys(person));
Reflect.deleteProperty - sebuah alternatif untuk operator hapus , dibuat dalam bentuk metode:
let person = { name: '', surname: '' }; delete person.name;
Reflect.has - sebuah alternatif untuk operator in , dibuat dalam bentuk metode:
let person = { name: '', surname: '' }; console.log('name' in person);
Reflect.get dan Reflect.set - untuk membaca / mengubah properti objek:
let person = { name: '', surname: '' }; console.log(Reflect.get(person, 'name'));
Rincian lebih lanjut tentang perubahan dapat ditemukan di sini .
Selain metode objek Refleksi yang tercantum di atas, ada proposal eksperimental untuk dengan mudah mengikat berbagai metadata ke objek.
Metadata dapat berupa informasi bermanfaat yang tidak terkait langsung dengan objek, misalnya:
Saat ini, polyfill ini digunakan untuk bekerja di browser .
5. Simbol
Simbol adalah tipe data abadi yang baru, terutama digunakan untuk membuat nama unik untuk pengidentifikasi properti objek. Kami memiliki kemampuan untuk membuat karakter dalam dua cara:
Simbol lokal - teks dalam parameter fungsi Simbol tidak memengaruhi keunikan dan hanya diperlukan untuk debugging:
const sym1 = Symbol('name'); const sym2 = Symbol('name'); console.log(sym1 == sym2);
Karakter global - karakter disimpan dalam registri global, sehingga karakter dengan kunci yang sama sama:
const sym3 = Symbol.for('name'); const sym4 = Symbol.for('name'); const sym5 = Symbol.for('other name'); console.log(sym3 == sym4);
Kemampuan untuk membuat pengidentifikasi semacam itu memungkinkan kita untuk tidak takut bahwa kita dapat menimpa beberapa properti dalam objek yang tidak kita ketahui. Kualitas ini memungkinkan pembuat standar untuk dengan mudah menambahkan properti standar baru ke objek, tanpa memutus kompatibilitas dengan berbagai pustaka yang ada (yang sudah bisa mendefinisikan properti yang sama) dan kode pengguna. Oleh karena itu, ada sejumlah simbol standar dan beberapa di antaranya memberikan peluang baru untuk refleksi:
Symbol.iterator - memungkinkan Anda untuk membuat aturan sendiri untuk iterasi objek menggunakan for-of atau ... operator spread :
let arr = [1, 2, 3];
Symbol.hasInstance adalah metode yang menentukan apakah konstruktor mengenali objek sebagai instance-nya. Digunakan oleh operator instance:
class MyArray { static [Symbol.hasInstance](instance) { return Array.isArray(instance); } } console.log([] instanceof MyArray);
Symbol.isConcatSpreadable - Menunjukkan apakah array harus diratakan saat digabungkan dalam Array.concat:
let firstArr = [1, 2, 3]; let secondArr = [4, 5, 6]; firstArr.concat(secondArr);
Symbol.species - memungkinkan Anda menentukan konstruktor mana yang akan digunakan untuk membuat objek turunan di dalam kelas.
Sebagai contoh, kami memiliki kelas Array standar untuk bekerja dengan array dan memiliki metode .map yang membuat array baru berdasarkan yang sekarang. Untuk mengetahui kelas mana yang digunakan untuk membuat larik baru ini, Array memanggil this.constructor [Symbol.species] seperti ini:
Array.prototype.map = function(cb) { const ArrayClass = this.constructor[Symbol.species]; const result = new ArrayClass(this.length); this.forEach((value, index, arr) => { result[index] = cb(value, index, arr); }); return result; }
Dengan demikian, mengesampingkan Symbol.species, kita dapat membuat kelas kita sendiri untuk bekerja dengan array dan mengatakan bahwa semua metode standar seperti .map, .reduce, dll. Tidak mengembalikan turunan dari kelas Array, tetapi turunan dari kelas kita:
class MyArray extends Array { static get [Symbol.species]() { return this; } } const arr = new MyArray(1, 2, 3);
Tentu saja, ini bekerja tidak hanya dengan array, tetapi juga dengan kelas standar lainnya. Selain itu, bahkan jika kita cukup membuat kelas kita sendiri dengan metode yang mengembalikan instance baru dari kelas yang sama, kita harus menggunakan this.constructor [Symbol.species] untuk mendapatkan referensi ke konstruktor.
Symbol.toPrimitive - memungkinkan Anda menentukan cara mengonversi objek kami ke nilai primitif. Jika sebelumnya, untuk mengurangi menjadi primitif, kita perlu menggunakan toString bersama dengan valueOf, sekarang semuanya bisa dilakukan dalam satu metode yang mudah:
const figure = { id: 1, name: '', [Symbol.toPrimitive](hint) { if (hint === 'string') { return this.name; } else if (hint === 'number') { return this.id; } else {
Symbol.match - memungkinkan Anda untuk membuat kelas handler Anda sendiri untuk metode untuk fungsi String.prototype.match :
class StartAndEndsWithMatcher { constructor(value) { this.value = value; } [Symbol.match](str) { const startsWith = str.startsWith(this.value); const endsWith = str.endsWith(this.value); if (startsWith && endsWith) { return [this.value]; } return null; } } const testMatchResult = '||'.match(new StartAndEndsWithMatcher('|')); console.log(testMatchResult);
β Symbol.replace , Symbol.search Symbol.split String.prototype .
, ( reflect-metadata ) . - , , . :
const validationRules = Symbol('validationRules'); const person = { name: '', surname: '' }; person[validationRules] = { name: ['max-length-256', 'required'], surname: ['max-length-256'] };
6. (Proxy)
Proxy , Reflect API Symbols ES6, // , , . , .
, data-binding MobX React, Vue . .
:
const formData = { login: 'User', password: 'pass' }; const proxyFormData = new Proxy(formData, { set(target, name, value) { target[name] = value; this.forceUpdate();
, /:
const formData = { login: 'User', password: 'pass' }; const proxyFormData = {}; for (let param in formData) { Reflect.defineProperty(proxyFormData, `__private__${param}`, { value: formData[param], enumerable: false, configurable: true }); Reflect.defineProperty(proxyFormData, param, { get: function() { return this[`__private__${param}`]; }, set: function(value) { this[`__private__${param}`] = value; this.forceUpdate();
-, β Proxy ( , ), / , delete obj[name] .
7.
JavaScript , ECMAScript 4, . , .
You Don't Know JS .