1. Pengenalan Proxy & Reflect
Proxy adalah fitur ES6 yang memungkinkan Anda membuat "pembungkus" (wrapper) di sekitar objek, array, atau fungsi. Proxy bisa menangkap dan mengubah perilaku dasar dari operasi pada objek — seperti membaca properti, menulis nilai, memanggil fungsi, dan lain-lain.
Reflect adalah API built-in yang menyediakan metode-metode untuk melakukan operasi dasar pada objek (get, set, has, deleteProperty, dll). Reflect dirancang untuk digunakan bersama Proxy agar handler bisa meneruskan operasi ke target secara default.
Mengapa Proxy Penting?
| Fitur | Penjelasan |
|---|---|
| Intercept Operasi | Menangkap get, set, delete, has, dan operasi lainnya |
| Validasi | Validasi data secara otomatis sebelum disimpan |
| Logging & Debug | Track semua akses dan perubahan properti |
| Reactive System | Membangun sistem reaktif seperti Vue.js 3 |
| Default Values | Menyediakan default untuk properti yang tidak ada |
| Access Control | Membatasi akses ke properti tertentu (private/readonly) |
Browser Support
Proxy dan Reflect didukung di semua browser modern (Chrome 49+, Firefox 18+, Safari 10+, Edge 12+). Proxy tidak bisa di-polyfill karena ini adalah fitur bahasa tingkat rendah. Jika Anda perlu support IE11, Proxy bukan pilihan.
Syntax Dasar Proxy
// Syntax: new Proxy(target, handler)
// target = objek yang ingin dibungkus
// handler = objek yang berisi traps (perangkap)
const target = {
nama: 'Beebane',
umur: 25
};
const handler = {}; // Handler kosong — tidak ada traps
const proxy = new Proxy(target, handler);
// Proxy bertindak persis seperti target
console.log(proxy.nama); // → 'Beebane'
console.log(proxy.umur); // → 25
// Operasi pada proxy mempengaruhi target
proxy.kota = 'Jakarta';
console.log(target.kota); // → 'Jakarta'
2. Cara Kerja Proxy
Proxy bekerja dengan menangkap operasi dasar (fundamental operations) pada objek target. Setiap operasi dasar memiliki trap (perangkap) yang bisa Anda definisikan di handler.
Kode Anda → Proxy → Handler (Traps?) → Target
↓
Jika ada trap:
Jalankan logika kustom
↓
Jika tidak ada trap:
Jalankan operasi default (via Reflect)
Contoh:
proxy.nama → Proxy → handler.get() ada?
↓
Ya → jalankan handler.get()
Tidak → Reflect.get(target, 'nama')
Daftar Lengkap Traps
// ===== 13 Trap yang tersedia di Proxy =====
const handler = {
// Membaca properti → get(target, prop, receiver)
get(target, prop, receiver) {},
// Menulis properti → set(target, prop, value, receiver)
set(target, prop, value, receiver) {},
// Cek 'in' operator → has(target, prop)
has(target, prop) {},
// Hapus properti → deleteProperty(target, prop)
deleteProperty(target, prop) {},
// Object.keys() → ownKeys(target)
ownKeys(target) {},
// Object.getOwnPropertyDescriptor() → getOwnPropertyDescriptor(target, prop)
getOwnPropertyDescriptor(target, prop) {},
// Object.defineProperty() → defineProperty(target, prop, descriptor)
defineProperty(target, prop, descriptor) {},
// Object.getPrototypeOf() → getPrototypeOf(target)
getPrototypeOf(target) {},
// Object.setPrototypeOf() → setPrototypeOf(target, proto)
setPrototypeOf(target, proto) {},
// Object.preventExtensions() → preventExtensions(target)
preventExtensions(target) {},
// Object.isExtensible() → isExtensible(target)
isExtensible(target) {},
// Fungsi dipanggil → apply(target, thisArg, args)
apply(target, thisArg, args) {},
// new Operator → construct(target, args, newTarget)
construct(target, args, newTarget) {}
};
Proxy memiliki invariants — aturan ketat yang HARUS dipatuhi oleh trap. Misalnya: trap set harus mengembalikan true/false, trap get untuk properti non-writable non-configurable harus mengembalikan nilai yang sama dengan target. Pelanggaran invariant akan melempar TypeError.
3. Get & Set Traps
Trap get dan set adalah dua trap yang paling sering digunakan. Mereka menangkap operasi pembacaan dan penulisan properti pada objek.
Get Trap — Membaca Properti
// ===== Contoh 1: Default Values =====
const data = { nama: 'Beebane' };
const proxyData = new Proxy(data, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
return `Properti '${prop}' tidak ditemukan`;
}
});
console.log(proxyData.nama); // → 'Beebane'
console.log(proxyData.umur); // → "Properti 'umur' tidak ditemukan"
// ===== Contoh 2: Logging / Audit Trail =====
const pengaturan = {
tema: 'dark',
bahasa: 'id',
fontSize: 16
};
const loggedPengaturan = new Proxy(pengaturan, {
get(target, prop, receiver) {
console.log(`[GET] Membaca properti: ${prop}`);
console.log(`[GET] Nilai: ${target[prop]}`);
return Reflect.get(target, prop, receiver);
}
});
loggedPengaturan.tema;
// Output:
// [GET] Membaca properti: tema
// [GET] Nilai: dark
// ===== Contoh 3: Nested Proxy (Deep Proxy) =====
function deepProxy(obj, handler) {
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
obj[key] = deepProxy(obj[key], handler);
}
}
return new Proxy(obj, handler);
}
const nested = deepProxy(
{ user: { profile: { nama: 'Beebane' } } },
{
get(target, prop) {
console.log(`[DEEP GET] ${prop}`);
return Reflect.get(target, prop);
}
}
);
nested.user.profile.nama;
// [DEEP GET] user
// [DEEP GET] profile
// [DEEP GET] nama
Set Trap — Menulis Properti
// ===== Contoh 1: Type Checking =====
const userProxy = new Proxy({}, {
set(target, prop, value, receiver) {
// Validasi tipe berdasarkan nama properti
if (prop === 'nama' && typeof value !== 'string') {
throw new TypeError(`'${prop}' harus berupa string`);
}
if (prop === 'umur' && typeof value !== 'number') {
throw new TypeError(`'${prop}' harus berupa number`);
}
if (prop === 'umur' && (value < 0 || value > 150)) {
throw new RangeError(`'${prop}' harus antara 0-150`);
}
console.log(`[SET] ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
userProxy.nama = 'Beebane'; // OK → [SET] nama = Beebane
userProxy.umur = 25; // OK → [SET] umur = 25
// userProxy.umur = -5; // ❌ RangeError!
// userProxy.nama = 123; // ❌ TypeError!
// ===== Contoh 2: Readonly Properties =====
const config = new Proxy({ host: 'localhost', port: 3000 }, {
set(target, prop, value, receiver) {
// Semua properti menjadi readonly setelah inisialisasi
console.warn(`[WARN] Properti '${prop}' bersifat readonly!`);
return false; // Menolak penulisan
}
});
config.host = 'example.com'; // → [WARN] Properti 'host' bersifat readonly!
console.log(config.host); // → 'localhost' (tidak berubah!)
4. Has & Delete Traps
Trap has menangkap operator in, sedangkan deleteProperty menangkap operasi delete.
// ===== Has Trap: Menyembunyikan Properti =====
const rahasia = {
nama: 'Beebane',
_password: 'rahasia123',
_apiKey: 'xyz-abc-123'
};
const amanRahasia = new Proxy(rahasia, {
has(target, prop) {
// Sembunyikan properti yang diawali underscore
if (prop.startsWith('_')) {
console.log(`[HAS] Properti '${prop}' disembunyikan`);
return false;
}
return Reflect.has(target, prop);
}
});
console.log('nama' in amanRahasia); // → true
console.log('_password' in amanRahasia); // → false (disembunyikan!)
console.log('_apiKey' in amanRahasia); // → false (disembunyikan!)
// ===== Delete Trap: Mencegah Penghapusan =====
const konstanta = new Proxy(
{ API_URL: 'https://api.example.com', VERSION: '2.0' },
{
deleteProperty(target, prop) {
throw new Error(`Tidak bisa menghapus konstanta '${prop}'`);
}
}
);
// delete konstanta.API_URL; // ❌ Error: Tidak bisa menghapus konstanta 'API_URL'
// ===== OwnKeys Trap: Filter Keys =====
const dataUser = {
id: 1,
nama: 'Beebane',
email: 'bee@example.com',
_token: 'secret-token',
_session: 'abc123'
};
const filteredProxy = new Proxy(dataUser, {
ownKeys(target) {
// Hanya kembalikan key yang BUKAN private
return Reflect.ownKeys(target).filter(
key => !key.startsWith('_')
);
}
});
console.log(Object.keys(filteredProxy));
// → ['id', 'nama', 'email'] (tanpa _token dan _session)
5. Apply & Construct Traps
Trap apply menangkap pemanggilan fungsi, sedangkan construct menangkap penggunaan new operator.
// ===== Apply Trap: Memodifikasi Fungsi =====
// Contoh 1: Memoization (Cache Hasil)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Bungkus dengan cache
const cache = new Map();
const cachedFib = new Proxy(fibonacci, {
apply(target, thisArg, args) {
const key = args.toString();
if (cache.has(key)) {
console.log(`[CACHE HIT] fib(${key})`);
return cache.get(key);
}
console.log(`[COMPUTE] fib(${key})`);
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
console.log(cachedFib(10)); // [COMPUTE] → 55
console.log(cachedFib(10)); // [CACHE HIT] → 55
// Contoh 2: Logging Function Calls
function tambah(a, b) { return a + b; }
function kali(a, b) { return a * b; }
function withLogging(fn, name) {
return new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`[CALL] ${name}(${args.join(', ')})`);
const result = Reflect.apply(target, thisArg, args);
console.log(`[RETURN] ${name} → ${result}`);
return result;
}
});
}
const loggedTambah = withLogging(tambah, 'tambah');
loggedTambah(5, 3);
// [CALL] tambah(5, 3)
// [RETURN] tambah → 8
// ===== Construct Trap: Memodifikasi new =====
class Orang {
constructor(nama, umur) {
this.nama = nama;
this.umur = umur;
}
}
const ProxyOrang = new Proxy(Orang, {
construct(target, args, newTarget) {
console.log(`[NEW] Membuat instance: ${args.join(', ')}`);
const instance = Reflect.construct(target, args, newTarget);
instance.dibuatPada = new Date(); // Tambah properti otomatis
return instance;
}
});
const user = new ProxyOrang('Beebane', 25);
console.log(user.nama); // → 'Beebane'
console.log(user.dibuatPada); // → Date object
6. Reflect API Lengkap
Reflect adalah objek built-in yang menyediakan metode untuk melakukan operasi dasar objek secara terstandarisasi. Sebelum Reflect, operasi ini tersebar di berbagai tempat (Object methods, operator, dll).
// ===== Mengapa Menggunakan Reflect? =====
// 1. Reflect.get(target, prop, receiver)
const obj = { nama: 'Beebane', get info() { return this.nama; } };
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
// receiver memastikan 'this' benar di getter
return Reflect.get(target, prop, receiver);
}
});
console.log(proxy.info); // → 'Beebane' (benar!)
// 2. Reflect.set(target, prop, value, receiver)
const counter = { _val: 0, set nilai(v) { this._val = v; } };
Reflect.set(counter, 'nilai', 42);
console.log(counter._val); // → 42
// 3. Reflect.has(target, prop) — mirip 'in' operator
console.log(Reflect.has({ a: 1 }, 'a')); // → true
console.log(Reflect.has({ a: 1 }, 'b')); // → false
// 4. Reflect.deleteProperty(target, prop)
const data = { a: 1, b: 2 };
Reflect.deleteProperty(data, 'a');
console.log(data); // → { b: 2 }
// 5. Reflect.ownKeys(target)
const mixed = { a: 1, [Symbol('id')]: 99 };
console.log(Reflect.ownKeys(mixed)); // → ['a', Symbol(id)]
// 6. Reflect.construct(target, args)
class Animal {
constructor(name) { this.name = name; }
}
const cat = Reflect.construct(Animal, ['Kucing']);
console.log(cat.name); // → 'Kucing'
// 7. Reflect.apply(target, thisArg, args)
function greet(greeting) {
return `${greeting}, ${this.nama}!`;
}
const result = Reflect.apply(greet, { nama: 'Beebane' }, ['Halo']);
console.log(result); // → 'Halo, Beebane!'
// 8. Reflect.defineProperty(target, prop, descriptor)
const obj2 = {};
Reflect.defineProperty(obj2, 'x', {
value: 10,
writable: false,
enumerable: true,
configurable: false
});
console.log(obj2.x); // → 10
// 9. Reflect.getPrototypeOf(target) & setPrototypeOf
const proto = { greet() { return 'Hello'; } };
const obj3 = {};
Reflect.setPrototypeOf(obj3, proto);
console.log(obj3.greet()); // → 'Hello'
// 10. Reflect.preventExtensions(target) & isExtensible
const sealed = { a: 1 };
Reflect.preventExtensions(sealed);
console.log(Reflect.isExtensible(sealed)); // → false
Dibandingkan metode Object seperti Object.getOwnPropertyDescriptor(), Reflect methods selalu mengembalikan boolean untuk operasi yang sebelumnya melempar error (seperti defineProperty). Ini membuat kode lebih bersih dan konsisten.
7. Validasi dengan Proxy
Salah satu use case paling populer dari Proxy adalah validasi data. Anda bisa membuat skema validasi yang otomatis mengecek tipe, range, dan format data saat properti diubah.
// ===== Schema Validator menggunakan Proxy =====
function createValidatedObject(schema) {
const data = {};
return new Proxy(data, {
set(target, prop, value, receiver) {
// Cek apakah properti ada di schema
if (!(prop in schema)) {
throw new Error(`Properti '${prop}' tidak ada dalam schema`);
}
const rules = schema[prop];
// Validasi tipe
if (rules.type && typeof value !== rules.type) {
throw new TypeError(
`'${prop}' harus bertipe ${rules.type}, diterima: ${typeof value}`
);
}
// Validasi required
if (rules.required && (value === null || value === undefined)) {
throw new Error(`'${prop}' wajib diisi`);
}
// Validasi min/max untuk number
if (rules.type === 'number') {
if (rules.min !== undefined && value < rules.min) {
throw new RangeError(`'${prop}' minimal ${rules.min}`);
}
if (rules.max !== undefined && value > rules.max) {
throw new RangeError(`'${prop}' maksimal ${rules.max}`);
}
}
// Validasi minLength/maxLength untuk string
if (rules.type === 'string') {
if (rules.minLength && value.length < rules.minLength) {
throw new Error(`'${prop}' minimal ${rules.minLength} karakter`);
}
if (rules.maxLength && value.length > rules.maxLength) {
throw new Error(`'${prop}' maksimal ${rules.maxLength} karakter`);
}
}
// Validasi pattern (regex)
if (rules.pattern && !rules.pattern.test(value)) {
throw new Error(`'${prop}' tidak sesuai format`);
}
return Reflect.set(target, prop, value, receiver);
}
});
}
// Definisikan schema
const userSchema = {
nama: { type: 'string', required: true, minLength: 2, maxLength: 50 },
umur: { type: 'number', required: true, min: 0, max: 150 },
email: {
type: 'string',
required: true,
pattern: /^[\w.-]+@[\w.-]+\.\w+$/
}
};
const user = createValidatedObject(userSchema);
user.nama = 'Beebane'; // ✅ OK
user.umur = 25; // ✅ OK
user.email = 'test@mail.com'; // ✅ OK
// user.nama = 123; // ❌ TypeError
// user.umur = -1; // ❌ RangeError
// user.email = 'invalid'; // ❌ Error: format
// user.unknown = 'test'; // ❌ Error: tidak ada dalam schema
// ===== Strict Object: Cegah Akses Properti Tak Terduga =====
function strict(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
throw new ReferenceError(
`Properti '${prop}' tidak ditemukan. ` +
`Properti yang tersedia: ${Object.keys(target).join(', ')}`
);
},
set(target, prop, value, receiver) {
if (!(prop in target)) {
throw new ReferenceError(
`Tidak bisa menambah properti baru '${prop}'`
);
}
return Reflect.set(target, prop, value, receiver);
}
});
}
const konfig = strict({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
});
console.log(konfig.apiUrl); // ✅ OK
// console.log(konfig.url); // ❌ ReferenceError!
8. Reactive Objects
Proxy adalah fondasi dari sistem reaktif seperti Vue.js 3. Dengan Proxy, Anda bisa mendeteksi perubahan data dan secara otomatis memperbarui UI atau menjalankan efek samping (side effects).
// ===== Reactive System Sederhana =====
function reactive(target) {
const listeners = new Map(); // prop → Set of callbacks
function notify(prop) {
if (listeners.has(prop)) {
listeners.get(prop).forEach(cb => cb(prop, target[prop]));
}
}
return new Proxy(target, {
get(target, prop, receiver) {
// Jika prop adalah method untuk subscribe
if (prop === '_subscribe') {
return (propName, callback) => {
if (!listeners.has(propName)) {
listeners.set(propName, new Set());
}
listeners.get(propName).add(callback);
};
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
notify(prop);
}
return result;
}
});
}
// Gunakan reactive
const state = reactive({
count: 0,
nama: 'Beebane'
});
// Subscribe ke perubahan
state._subscribe('count', (prop, newVal) => {
console.log(`[REACT] ${prop} berubah menjadi: ${newVal}`);
});
state._subscribe('nama', (prop, newVal) => {
console.log(`[REACT] ${prop} berubah menjadi: ${newVal}`);
});
// Trigger reactive
state.count = 1; // [REACT] count berubah menjadi: 1
state.count = 2; // [REACT] count berubah menjadi: 2
state.nama = 'BL'; // [REACT] nama berubah menjadi: BL
// ===== Computed Properties dengan Proxy =====
function computed(target, computeds) {
return new Proxy(target, {
get(target, prop, receiver) {
// Cek apakah properti adalah computed
if (prop in computeds) {
return computeds[prop](target);
}
return Reflect.get(target, prop, receiver);
}
});
}
const data = computed(
{ harga: 100000, pajak: 0.11 },
{
totalPajak: (d) => d.harga * d.pajak,
hargaAkhir: (d) => d.harga + d.harga * d.pajak,
formatted: (d) => `Rp ${(d.harga + d.harga * d.pajak).toLocaleString('id')}`
}
);
console.log(data.totalPajak); // → 11000
console.log(data.hargaAkhir); // → 111000
console.log(data.formatted); // → "Rp 111.000"
// Computed otomatis berubah saat data berubah
data.harga = 200000;
console.log(data.hargaAkhir); // → 222000
9. Use Cases Lanjutan
Negative Array Indexing
// Mirip Python: arr[-1] mendapatkan elemen terakhir
function negativeArray(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
const index = Number(prop);
if (Number.isInteger(index) && index < 0) {
return Reflect.get(target, target.length + index, receiver);
}
return Reflect.get(target, prop, receiver);
}
});
}
const items = negativeArray(['a', 'b', 'c', 'd', 'e']);
console.log(items[-1]); // → 'e' (elemen terakhir)
console.log(items[-2]); // → 'd'
Observable (Watch All Changes)
// ===== Observable: Watch Semua Perubahan =====
function observable(target, onChange) {
return new Proxy(target, {
set(obj, prop, value, receiver) {
const oldValue = obj[prop];
const result = Reflect.set(obj, prop, value, receiver);
if (oldValue !== value) {
onChange({
type: 'set',
prop,
oldValue,
newValue: value
});
}
return result;
},
deleteProperty(obj, prop) {
const oldValue = obj[prop];
const result = Reflect.deleteProperty(obj, prop);
onChange({
type: 'delete',
prop,
oldValue
});
return result;
}
});
}
const state = observable({ count: 0, items: [] }, (change) => {
console.log('[CHANGE]', change);
});
state.count = 1;
// [CHANGE] { type: 'set', prop: 'count', oldValue: 0, newValue: 1 }
delete state.count;
// [CHANGE] { type: 'delete', prop: 'count', oldValue: 1 }
Revocable Proxy
// ===== Revocable Proxy: Bisa Dinonaktifkan =====
const sensitiveData = { apiKey: 'secret-123', token: 'bearer-abc' };
const { proxy, revoke } = Proxy.revocable(sensitiveData, {
get(target, prop) {
console.log(`[ACCESS] ${prop}`);
return Reflect.get(target, prop);
}
});
// Bisa diakses
console.log(proxy.apiKey); // → 'secret-123'
// Revoke (nonaktifkan proxy)
revoke();
// Semua operasi sekarang error!
// proxy.apiKey; // ❌ TypeError: proxy has been revoked
// proxy.token; // ❌ TypeError: proxy has been revoked
Proxy untuk Debugging
// ===== Trace Helper: Log Semua Operasi pada Object =====
function trace(obj, name = 'Object') {
return new Proxy(obj, {
get(target, prop, receiver) {
console.log(`[${name}] GET ${String(prop)}`);
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return function(...args) {
console.log(`[${name}] CALL ${String(prop)}(${args})`);
return Reflect.apply(value, target, args);
};
}
return value;
},
set(target, prop, value, receiver) {
console.log(`[${name}] SET ${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
}
const arr = trace([1, 2, 3], 'Array');
arr.push(4);
// [Array] GET push
// [Array] CALL push(4)
console.log(arr.length);
// [Array] GET length
// → 4
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Proxy & Reflect: