Panduan komprehensif untuk developer global dalam menguasai API Proxy JavaScript. Pelajari cara mencegat dan menyesuaikan operasi objek dengan contoh praktis, kasus penggunaan, dan tips performa.
API Proxy JavaScript: Penyelaman Mendalam ke Modifikasi Perilaku Objek
Dalam lanskap JavaScript modern yang terus berkembang, para developer terus mencari cara yang lebih kuat dan elegan untuk mengelola dan berinteraksi dengan data. Meskipun fitur seperti class, module, dan async/await telah merevolusi cara kita menulis kode, ada fitur metaprogramming yang kuat yang diperkenalkan di ECMAScript 2015 (ES6) yang sering kali kurang dimanfaatkan: API Proxy.
Metaprogramming mungkin terdengar mengintimidasi, tetapi ini hanyalah konsep menulis kode yang beroperasi pada kode lain. API Proxy adalah alat utama JavaScript untuk ini, memungkinkan Anda membuat 'proxy' untuk objek lain, yang dapat mencegat dan mendefinisikan ulang operasi fundamental untuk objek tersebut. Ini seperti menempatkan penjaga gerbang yang dapat disesuaikan di depan sebuah objek, memberi Anda kendali penuh atas cara objek tersebut diakses dan dimodifikasi.
Panduan komprehensif ini akan mengupas tuntas API Proxy. Kita akan menjelajahi konsep intinya, menguraikan berbagai kemampuannya dengan contoh praktis, dan membahas kasus penggunaan tingkat lanjut serta pertimbangan performa. Pada akhirnya, Anda akan memahami mengapa Proxy menjadi landasan kerangka kerja modern dan bagaimana Anda dapat memanfaatkannya untuk menulis kode yang lebih bersih, lebih kuat, dan lebih mudah dipelihara.
Memahami Konsep Inti: Target, Handler, dan Trap
API Proxy dibangun di atas tiga komponen fundamental. Memahami peran masing-masing adalah kunci untuk menguasai proxy.
- Target: Ini adalah objek asli yang ingin Anda bungkus. Objek ini bisa berupa jenis objek apa pun, termasuk array, fungsi, atau bahkan proxy lain. Proxy melakukan virtualisasi terhadap target ini, dan semua operasi pada akhirnya (meskipun tidak harus) diteruskan kepadanya.
- Handler: Ini adalah objek yang berisi logika untuk proxy. Ini adalah objek placeholder yang propertinya adalah fungsi, yang dikenal sebagai 'trap'. Ketika sebuah operasi terjadi pada proxy, ia akan mencari trap yang sesuai pada handler.
- Trap: Ini adalah metode pada handler yang menyediakan akses properti. Setiap trap sesuai dengan operasi objek fundamental. Sebagai contoh, trap
get
mencegat pembacaan properti, dan trapset
mencegat penulisan properti. Jika sebuah trap tidak didefinisikan pada handler, operasi tersebut hanya akan diteruskan ke target seolah-olah proxy tidak ada.
Sintaks untuk membuat proxy cukup sederhana:
const proxy = new Proxy(target, handler);
Mari kita lihat contoh yang sangat dasar. Kita akan membuat sebuah proxy yang hanya meneruskan semua operasi ke objek target dengan menggunakan handler kosong.
// Objek asli
const target = {
message: "Hello, World!"
};
// Handler kosong. Semua operasi akan diteruskan ke target.
const handler = {};
// Objek proxy
const proxy = new Proxy(target, handler);
// Mengakses properti pada proxy
console.log(proxy.message); // Output: Hello, World!
// Operasi diteruskan ke target
console.log(target.message); // Output: Hello, World!
// Memodifikasi properti melalui proxy
proxy.anotherMessage = "Hello, Proxy!";
console.log(proxy.anotherMessage); // Output: Hello, Proxy!
console.log(target.anotherMessage); // Output: Hello, Proxy!
Dalam contoh ini, proxy berperilaku persis seperti objek asli. Kekuatan sebenarnya muncul ketika kita mulai mendefinisikan trap di dalam handler.
Anatomi Proxy: Menjelajahi Trap Umum
Objek handler dapat berisi hingga 13 trap yang berbeda, masing-masing sesuai dengan metode internal fundamental dari objek JavaScript. Mari kita jelajahi yang paling umum dan berguna.
Trap Akses Properti
1. `get(target, property, receiver)`
Ini bisa dibilang trap yang paling sering digunakan. Trap ini dipicu ketika sebuah properti dari proxy dibaca.
target
: Objek asli.property
: Nama properti yang sedang diakses.receiver
: Proxy itu sendiri, atau objek yang mewarisinya.
Contoh: Nilai default untuk properti yang tidak ada.
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const userHandler = {
get(target, property) {
// Jika properti ada di target, kembalikan nilainya.
// Jika tidak, kembalikan pesan default.
return property in target ? target[property] : `Properti '${property}' tidak ada.`;
}
};
const userProxy = new Proxy(user, userHandler);
console.log(userProxy.firstName); // Output: John
console.log(userProxy.age); // Output: 30
console.log(userProxy.country); // Output: Properti 'country' tidak ada.
2. `set(target, property, value, receiver)`
Trap set
dipanggil ketika sebuah properti dari proxy diberi nilai. Ini sempurna untuk validasi, pencatatan (logging), atau membuat objek read-only.
value
: Nilai baru yang diberikan ke properti.- Trap harus mengembalikan boolean:
true
jika penugasan berhasil, danfalse
jika tidak (yang akan melemparTypeError
dalam strict mode).
Contoh: Validasi data.
const person = {
name: 'Jane Doe',
age: 25
};
const validationHandler = {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeError('Usia harus berupa bilangan bulat.');
}
if (value <= 0) {
throw new RangeError('Usia harus berupa angka positif.');
}
}
// Jika validasi lolos, atur nilai pada objek target.
target[property] = value;
// Tunjukkan keberhasilan.
return true;
}
};
const personProxy = new Proxy(person, validationHandler);
personProxy.age = 30; // Ini valid
console.log(personProxy.age); // Output: 30
try {
personProxy.age = 'thirty'; // Melempar TypeError
} catch (e) {
console.error(e.message); // Output: Usia harus berupa bilangan bulat.
}
try {
personProxy.age = -5; // Melempar RangeError
} catch (e) {
console.error(e.message); // Output: Usia harus berupa angka positif.
}
3. `has(target, property)`
Trap ini mencegat operator in
. Ini memungkinkan Anda untuk mengontrol properti mana yang tampak ada pada sebuah objek.
Contoh: Menyembunyikan properti 'pribadi'.
Di JavaScript, konvensi umum adalah memberi awalan garis bawah (_) pada properti pribadi. Kita bisa menggunakan trap has
untuk menyembunyikannya dari operator in
.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const hidingHandler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // Berpura-pura properti itu tidak ada
}
return property in target;
}
};
const dataProxy = new Proxy(secretData, hidingHandler);
console.log('publicKey' in dataProxy); // Output: true
console.log('_apiKey' in dataProxy); // Output: false (meskipun ada di target)
console.log('id' in dataProxy); // Output: true
Catatan: Ini hanya memengaruhi operator in
. Akses langsung seperti dataProxy._apiKey
akan tetap berfungsi kecuali Anda juga mengimplementasikan trap get
yang sesuai.
4. `deleteProperty(target, property)`
Trap ini dieksekusi ketika sebuah properti dihapus menggunakan operator delete
. Ini berguna untuk mencegah penghapusan properti penting.
Trap harus mengembalikan true
untuk penghapusan yang berhasil atau false
jika gagal.
Contoh: Mencegah penghapusan properti.
const immutableConfig = {
databaseUrl: 'prod.db.server',
port: 8080
};
const deletionGuardHandler = {
deleteProperty(target, property) {
if (property in target) {
console.warn(`Upaya menghapus properti yang dilindungi: '${property}'. Operasi ditolak.`);
return false;
}
return true; // Properti memang tidak ada
}
};
const configProxy = new Proxy(immutableConfig, deletionGuardHandler);
delete configProxy.port;
// Output konsol: Upaya menghapus properti yang dilindungi: 'port'. Operasi ditolak.
console.log(configProxy.port); // Output: 8080 (Properti tidak dihapus)
Trap Enumerasi dan Deskripsi Objek
5. `ownKeys(target)`
Trap ini dipicu oleh operasi yang mendapatkan daftar properti milik objek itu sendiri, seperti Object.keys()
, Object.getOwnPropertyNames()
, Object.getOwnPropertySymbols()
, dan Reflect.ownKeys()
.
Contoh: Menyaring kunci (keys).
Mari kita gabungkan ini dengan contoh properti 'pribadi' sebelumnya untuk menyembunyikannya sepenuhnya.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const keyHidingHandler = {
has(target, property) {
return !property.startsWith('_') && property in target;
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
},
get(target, property, receiver) {
// Juga mencegah akses langsung
if (property.startsWith('_')) {
return undefined;
}
return Reflect.get(target, property, receiver);
}
};
const fullProxy = new Proxy(secretData, keyHidingHandler);
console.log(Object.keys(fullProxy)); // Output: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Output: true
console.log('_apiKey' in fullProxy); // Output: false
console.log(fullProxy._apiKey); // Output: undefined
Perhatikan bahwa kita menggunakan Reflect
di sini. Objek Reflect
menyediakan metode untuk operasi JavaScript yang dapat dicegat, dan metodenya memiliki nama dan tanda tangan yang sama dengan trap proxy. Merupakan praktik terbaik untuk menggunakan Reflect
untuk meneruskan operasi asli ke target, memastikan perilaku default dipertahankan dengan benar.
Trap Fungsi dan Konstruktor
Proxy tidak terbatas pada objek biasa. Ketika target adalah sebuah fungsi, Anda dapat mencegat panggilan dan konstruksi.
6. `apply(target, thisArg, argumentsList)`
Trap ini dipanggil ketika proxy dari sebuah fungsi dieksekusi. Ini mencegat panggilan fungsi.
target
: Fungsi asli.thisArg
: Konteksthis
untuk panggilan tersebut.argumentsList
: Daftar argumen yang diteruskan ke fungsi.
Contoh: Mencatat panggilan fungsi dan argumennya.
function sum(a, b) {
return a + b;
}
const loggingHandler = {
apply(target, thisArg, argumentsList) {
console.log(`Memanggil fungsi '${target.name}' dengan argumen: ${argumentsList}`);
// Jalankan fungsi asli dengan konteks dan argumen yang benar
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`Fungsi '${target.name}' mengembalikan: ${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, loggingHandler);
proxiedSum(5, 10);
// Output konsol:
// Memanggil fungsi 'sum' dengan argumen: 5,10
// Fungsi 'sum' mengembalikan: 15
7. `construct(target, argumentsList, newTarget)`
Trap ini mencegat penggunaan operator new
pada proxy dari sebuah kelas atau fungsi.
Contoh: Implementasi pola Singleton.
class MyDatabaseConnection {
constructor(url) {
this.url = url;
console.log(`Menyambung ke ${this.url}...`);
}
}
let instance;
const singletonHandler = {
construct(target, argumentsList) {
if (!instance) {
console.log('Membuat instance baru.');
instance = Reflect.construct(target, argumentsList);
}
console.log('Mengembalikan instance yang ada.');
return instance;
}
};
const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);
const conn1 = new ProxiedConnection('db://primary');
// Output konsol:
// Membuat instance baru.
// Menyambung ke db://primary...
// Mengembalikan instance yang ada.
const conn2 = new ProxiedConnection('db://secondary'); // URL akan diabaikan
// Output konsol:
// Mengembalikan instance yang ada.
console.log(conn1 === conn2); // Output: true
console.log(conn1.url); // Output: db://primary
console.log(conn2.url); // Output: db://primary
Kasus Penggunaan Praktis dan Pola Tingkat Lanjut
Setelah kita membahas trap secara individual, mari kita lihat bagaimana mereka dapat digabungkan untuk memecahkan masalah di dunia nyata.
1. Abstraksi API dan Transformasi Data
API sering kali mengembalikan data dalam format yang tidak sesuai dengan konvensi aplikasi Anda (misalnya, snake_case
vs. camelCase
). Sebuah proxy dapat secara transparan menangani konversi ini.
function snakeToCamel(s) {
return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}
// Bayangkan ini adalah data mentah kita dari API
const apiResponse = {
user_id: 123,
first_name: 'Alice',
last_name: 'Wonderland',
account_status: 'active'
};
const camelCaseHandler = {
get(target, property) {
const camelCaseProperty = snakeToCamel(property);
// Periksa apakah versi camelCase ada secara langsung
if (camelCaseProperty in target) {
return target[camelCaseProperty];
}
// Fallback ke nama properti asli
if (property in target) {
return target[property];
}
return undefined;
}
};
const userModel = new Proxy(apiResponse, camelCaseHandler);
// Sekarang kita bisa mengakses properti menggunakan camelCase, meskipun disimpan sebagai snake_case
console.log(userModel.userId); // Output: 123
console.log(userModel.firstName); // Output: Alice
console.log(userModel.accountStatus); // Output: active
2. Observable dan Data Binding (Inti dari Kerangka Kerja Modern)
Proxy adalah mesin di balik sistem reaktivitas dalam kerangka kerja modern seperti Vue 3. Ketika Anda mengubah properti pada objek state yang diproxy, trap set
dapat digunakan untuk memicu pembaruan di UI atau bagian lain dari aplikasi.
Berikut adalah contoh yang sangat disederhanakan:
function createObservable(target, callback) {
const handler = {
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
callback(prop, value); // Panggil callback saat ada perubahan
return result;
}
};
return new Proxy(target, handler);
}
const state = {
count: 0,
message: 'Hello'
};
function render(prop, value) {
console.log(`PERUBAHAN TERDETEKSI: Properti '${prop}' diatur menjadi '${value}'. Merender ulang UI...`);
}
const observableState = createObservable(state, render);
observableState.count = 1;
// Output konsol: PERUBAHAN TERDETEKSI: Properti 'count' diatur menjadi '1'. Merender ulang UI...
observableState.message = 'Goodbye';
// Output konsol: PERUBAHAN TERDETEKSI: Properti 'message' diatur menjadi 'Goodbye'. Merender ulang UI...
3. Indeks Array Negatif
Contoh klasik dan menyenangkan adalah memperluas perilaku array bawaan untuk mendukung indeks negatif, di mana -1
merujuk pada elemen terakhir, mirip dengan bahasa seperti Python.
function createNegativeArrayProxy(arr) {
const handler = {
get(target, property) {
const index = Number(property);
if (!Number.isNaN(index) && index < 0) {
// Konversi indeks negatif menjadi positif dari akhir array
property = String(target.length + index);
}
return Reflect.get(target, property);
}
};
return new Proxy(arr, handler);
}
const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);
console.log(proxiedArray[0]); // Output: a
console.log(proxiedArray[-1]); // Output: e
console.log(proxiedArray[-2]); // Output: d
console.log(proxiedArray.length); // Output: 5
Pertimbangan Performa dan Praktik Terbaik
Meskipun proxy sangat kuat, mereka bukanlah peluru ajaib. Sangat penting untuk memahami implikasinya.
Overhead Performa
Sebuah proxy memperkenalkan lapisan tidak langsung (indirection). Setiap operasi pada objek yang diproxy harus melewati handler, yang menambahkan sedikit overhead dibandingkan dengan operasi langsung pada objek biasa. Untuk sebagian besar aplikasi (seperti validasi data atau reaktivitas tingkat kerangka kerja), overhead ini dapat diabaikan. Namun, dalam kode yang sangat kritis terhadap performa, seperti loop ketat yang memproses jutaan item, ini bisa menjadi bottleneck. Selalu lakukan benchmark jika performa adalah perhatian utama.
Invarian Proxy
Sebuah trap tidak bisa sepenuhnya berbohong tentang sifat objek target. JavaScript memberlakukan serangkaian aturan yang disebut 'invarian' yang harus dipatuhi oleh trap proxy. Melanggar invarian akan menghasilkan TypeError
.
Sebagai contoh, invarian untuk trap deleteProperty
adalah bahwa ia tidak dapat mengembalikan true
(menandakan keberhasilan) jika properti yang sesuai pada objek target tidak dapat dikonfigurasi (non-configurable). Ini mencegah proxy mengklaim bahwa ia menghapus properti yang tidak dapat dihapus.
const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });
const handler = {
deleteProperty(target, prop) {
// Ini akan melanggar invarian
return true;
}
};
const proxy = new Proxy(target, handler);
try {
delete proxy.unbreakable; // Ini akan melempar error
} catch (e) {
console.error(e.message);
// Output: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}
Kapan Menggunakan Proxy (dan Kapan Tidak)
- Baik untuk: Membangun kerangka kerja dan pustaka (misalnya, manajemen state, ORM), debugging dan logging, mengimplementasikan sistem validasi yang kuat, dan membuat API canggih yang mengabstraksi struktur data yang mendasarinya.
- Pertimbangkan alternatif untuk: Algoritma yang kritis terhadap performa, ekstensi objek sederhana di mana kelas atau fungsi pabrik (factory function) sudah cukup, atau ketika Anda perlu mendukung browser yang sangat tua yang tidak memiliki dukungan ES6.
Proxy yang Dapat Dicabut (Revocable Proxies)
Untuk skenario di mana Anda mungkin perlu 'mematikan' sebuah proxy (misalnya, untuk alasan keamanan atau manajemen memori), JavaScript menyediakan Proxy.revocable()
. Ini mengembalikan sebuah objek yang berisi proxy dan fungsi revoke
.
const target = { data: 'sensitive' };
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.data); // Output: sensitive
// Sekarang, kita mencabut akses proxy
revoke();
try {
console.log(proxy.data); // Ini akan melempar error
} catch (e) {
console.error(e.message);
// Output: Cannot perform 'get' on a proxy that has been revoked
}
Proxy vs. Teknik Metaprogramming Lainnya
Sebelum ada Proxy, para developer menggunakan metode lain untuk mencapai tujuan serupa. Penting untuk memahami perbandingan Proxy dengan metode tersebut.
`Object.defineProperty()`
Object.defineProperty()
memodifikasi objek secara langsung dengan mendefinisikan getter dan setter untuk properti tertentu. Sebaliknya, Proxy sama sekali tidak memodifikasi objek asli; mereka membungkusnya.
- Cakupan: `defineProperty` bekerja berdasarkan per-properti. Anda harus mendefinisikan getter/setter untuk setiap properti yang ingin Anda pantau. Trap
get
danset
pada Proxy bersifat global, menangkap operasi pada setiap properti, termasuk properti baru yang ditambahkan kemudian. - Kemampuan: Proxy dapat mencegat jangkauan operasi yang lebih luas, seperti
deleteProperty
, operatorin
, dan panggilan fungsi, yang tidak dapat dilakukan oleh `defineProperty`.
Kesimpulan: Kekuatan Virtualisasi
API Proxy JavaScript lebih dari sekadar fitur cerdas; ini adalah pergeseran fundamental dalam cara kita dapat merancang dan berinteraksi dengan objek. Dengan memungkinkan kita untuk mencegat dan menyesuaikan operasi fundamental, Proxy membuka pintu ke dunia pola yang kuat: dari validasi dan transformasi data yang mulus hingga sistem reaktif yang menjadi kekuatan antarmuka pengguna modern.
Meskipun mereka datang dengan sedikit biaya performa dan serangkaian aturan yang harus diikuti, kemampuan mereka untuk menciptakan abstraksi yang bersih, terpisah, dan kuat tidak tertandingi. Dengan melakukan virtualisasi objek, Anda dapat membangun sistem yang lebih kuat, mudah dipelihara, dan ekspresif. Lain kali Anda menghadapi tantangan kompleks yang melibatkan manajemen data, validasi, atau observabilitas, pertimbangkan apakah Proxy adalah alat yang tepat untuk pekerjaan itu. Bisa jadi itu adalah solusi paling elegan di dalam perangkat Anda.