Bahasa Indonesia

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.

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.

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.

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.

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)

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.

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.