Di section Values dan Types, menyalin nilai terasa tidak bermasalah: let a = 5; let b = a; menghasilkan dua variabel yang berdiri sendiri. Object berperilaku berbeda — dan perbedaan inilah sumber salah satu bug paling sering ditemui pemula: dua variabel yang seharusnya terpisah ternyata menunjuk data yang sama, sehingga mengubah satu membuat yang lain ikut berubah.

Chapter ini membuktikan perilaku referensi itu lewat kode, lalu membahas dua teknik salinan yang benar: spread untuk shallow copy dan structuredClone untuk deep copy. Bagian akhirnya menjawab pertanyaan pendampingnya: karena === tidak membandingkan isi object, bagaimana cara membandingkan dua object dengan benar?

Referensi, Bukan Salinan #

Aturannya: nilai primitive (number, string, boolean, dan sebagainya) disalin by value — isi datanya yang diperbanyak. Object disalin by reference — yang diperbanyak hanyalah alamatnya, bukan isinya. Assignment const b = a pada object tidak membuat object baru; ia membuat nama kedua yang menunjuk object yang sama di memori.

Keputusan desain ini punya alasannya. Object bisa berukuran besar dan bertingkat dalam; menyalin seluruh isinya setiap kali di-assign akan memboros waktu dan memori, padahal sering kali yang kita inginkan justru membagikan data yang sama ke banyak tempat. Karena itu engine memilih membagikan alamat, dan JavaScript menyediakan alat eksplisit — spread, structuredClone — untuk saat salinan yang benar-benar terpisah memang diinginkan. Pemilihan alat yang tepat itulah inti chapter ini.

Akibatnya ada dua keadaan yang berbeda dan mudah tertukar: “dua variabel menunjuk satu object” dan “dua object berisi data yang sama persis”. Operator === pada object menjawab pertanyaan pertama saja — ia membandingkan alamat, bukan isi. Mari kita buktikan semuanya lewat kode.

Membuktikan Referensi dan Membandingkan Manual #

// uji-referensi.js
// Object menyimpan referensi, bukan salinan nilai.

const keranjangA = { total: 150000, item: 3 };
const keranjangB = keranjangA;               // referensi yang sama

console.log(keranjangA === keranjangB);      // true: satu object, dua nama

keranjangB.total = 175000;
console.log("A total:", keranjangA.total);   // ikut berubah
console.log("B total:", keranjangB.total);

// object terpisah, walaupun isinya sama persis
const keranjangC = { total: 175000, item: 3 };
console.log(keranjangA === keranjangC);      // false

// perbandingan manual: kunci dan nilai dibandingkan satu per satu
function samaKonten(a, b) {
  const kunciA = Object.keys(a);
  const kunciB = Object.keys(b);
  if (kunciA.length !== kunciB.length) {
    return false;
  }
  for (let i = 0; i < kunciA.length; i++) {
    if (a[kunciA[i]] !== b[kunciA[i]]) {
      return false;
    }
  }
  return true;
}

console.log(samaKonten(keranjangA, keranjangC)); // true: kontennya identik
console.log(samaKonten(keranjangA, keranjangB)); // true: memang referensi sama

Output yang Dihasilkan:

text
true
A total: 175000
B total: 175000
false
true
true

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 5–6 (const keranjangA = {...} dan const keranjangB = keranjangA;): tidak ada object kedua yang dibuat. keranjangB hanya menambah nama untuk object yang sama dengan keranjangA.
  2. Baris 8 (console.log(keranjangA === keranjangB);): true karena keduanya alamat yang sama. Untuk object, === menguji identitas referensi.
  3. Baris 10–12 (keranjangB.total = 175000; dan seterusnya): perubahan dilakukan lewat keranjangB, tetapi pembacaan lewat keranjangA juga menghasilkan 175000 — bukti bahwa hanya ada satu object di balik dua nama. Inilah akibat yang paling sering mengejutkan.
  4. Baris 15–16 (const keranjangC = {...} dan console.log(...)): object ketiga ditulis dengan isi yang sama persis. === tetap menghasilkan false — dua object literal selalu membuat object terpisah.
  5. Baris 19–30 (function samaKonten(a, b) { ... }): perbandingan manual. Jumlah kunci dibandingkan lebih dulu (object dengan jumlah properti berbeda pasti berbeda), lalu loop membaca setiap kunci dari a dan membandingkan nilainya dengan b. Ada satu kunci saja yang tidak sama, function langsung mengembalikan false.
  6. Baris 32–33 (console.log(samaKonten(...))): keranjangA dan keranjangC dinilai sama meskipun === menyatakan berbeda — keduanya memang berisi data yang identik. Pemanggilan kedua bernilai true karena object yang berreferensi sama tentu berisi sama.

Shallow Copy dengan Spread #

Spread dari chapter Spread Syntax menyalin properti tingkat pertama. Untuk properti yang isinya object lain, yang disalin tetap referensinya — itulah sebabnya salinan jenis ini disebut shallow copy:

// uji-kloning.js
// Spread menyalin tingkat pertama saja; structuredClone menyalin hingga dalam.

const profil = {
  nama: "Budi",
  kontak: {
    email: "[email protected]"
  }
};

// spread: shallow copy — nested masih ditunjuk bersama
const salinanSpread = { ...profil };
salinanSpread.nama = "Budi S.";                      // aman: properti atas terpisah
salinanSpread.kontak.email = "[email protected]";   // nested ikut terdampak

console.log("Spread - nama profil :", profil.nama);
console.log("Spread - email profil:", profil.kontak.email);

// structuredClone: deep copy — nested benar-benar terpisah
const klon = structuredClone(profil);
klon.kontak.email = "[email protected]";
console.log("Clone - email profil :", profil.kontak.email);
console.log("Clone - email klon   :", klon.kontak.email);

// structuredClone menangani Date dengan benar
const dokumen = {
  judul: "Laporan Kuartal 1",
  dibuat: new Date(2026, 0, 15)
};
const klonDokumen = structuredClone(dokumen);
klonDokumen.dibuat.setFullYear(2030);
console.log("Tanggal asli :", dokumen.dibuat.getFullYear());
console.log("Tanggal klon :", klonDokumen.dibuat.getFullYear());

Output yang Dihasilkan:

text
Spread - nama profil : Budi
Spread - email profil: [email protected]
Clone - email profil : [email protected]
Clone - email klon   : [email protected]
Tanggal asli : 2026
Tanggal klon : 2030

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 13–15 (const salinanSpread = { ...profil }; dan dua baris berikutnya): spread membuat object baru, sehingga mengganti nama pada salinan aman dari profil.
  2. Baris 17–18 (console.log("Spread - nama profil :", ...) dan seterusnya): bukti sifat shallow — nama asli bertahan ("Budi"), tetapi profil.kontak.email ikut menjadi "[email protected]". Properti kontak tidak disalin ulang; asal dan salinan menunjuk object kontak yang sama.
  3. Baris 21–24 (const klon = structuredClone(profil); dan seterusnya): structuredClone menyalin seluruh kedalaman struktur. Mengubah klon.kontak.email kini tidak memengaruhi profil — kedua email terpisah seutuhnya.
  4. Baris 27–33 (const dokumen = {...} sampai console.log(...)): Date adalah object, sehingga shallow copy akan menunjuk tanggal yang sama. structuredClone menggandakan Date-nya juga: mengubah tahun pada klon menjadi 2030 tidak menggeser dokumen asli yang tetap 2026.

structuredClone juga menangani struktur lain yang berisi referensi, seperti Map dan Set (section Collections) serta referensi melingkar — object yang di dalamnya memuat dirinya sendiri. Untuk data biasa berisi primitive dan object polos, spread dan structuredClone menghasilkan data yang sama; perbedaannya hanya terasa pada tingkat kedalaman.

Alternatif ketiga untuk shallow copy adalah Object.assign({}, sumber) dari chapter Object Methods. Sifatnya identik dengan spread — dan batasannya pun identik:

js
const asal = { tipe: "meja", laci: { jumlah: 3 } };
const salinan = Object.assign({}, asal);
salinan.laci.jumlah = 5;
console.log(asal.laci.jumlah);

Output yang Dihasilkan:

text
5

Properti tipe pada asal aman dari perubahan salinan, tetapi laci tidak: object laci itu tetap ditunjuk bersama, sehingga angka 5 juga terbaca dari asal. Tiga teknik penyalinan — spread, Object.assign, dan manual — sama-sama berhenti di tingkat pertama. Hanya structuredClone yang turun sampai ke dasar.

Batas Perbandingan Manual #

Function samaKonten di atas membandingkan nilai dengan !==. Untuk properti yang isinya object lain, !== kembali membandingkan referensi — sehingga dua object identik tetapi nested bisa dinilai berbeda:

js
const a = { info: { umur: 20 } };
const b = { info: { umur: 20 } };
console.log(a.info === b.info);

Output yang Dihasilkan:

text
false

Perbandingan isi yang benar-benar menyeluruh harus turun ke setiap tingkat — pekerjaan yang di kode nyata biasanya diserahkan ke pustaka perbandingan atau diperiksa field demi field yang memang relevan. Pilihan terakhir itulah yang paling sering benar: alih-alih menanya “apakah dua object ini identik”, kebutuhan nyata biasanya lebih spesifik — “apakah dua pesanan punya item yang sama”, “apakah profil sudah disunting” — dan masing-masing pertanyaan itu punya definisi sendiri tentang field mana yang dibandingkan. Function perbandingan buatan sendiri, seperti samaKonten, cukup ditulis untuk pertanyaan yang benar-benar ada di program kita.

Kunci dari seluruh chapter ini sederhana: tanyakan selalu apakah yang sedang dibandingkan atau disalin adalah isi atau alamat.

Chapter penutup section ini merangkum kebiasaan baik lainnya: menata object agar tetap ringkas, konsisten, dan mudah dipelihara.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

uji-kloning.js

Unduh berkas
javascript
// uji-kloning.js
// Spread menyalin tingkat pertama saja; structuredClone menyalin hingga dalam.

const profil = {
  nama: "Budi",
  kontak: {
    email: "[email protected]"
  }
};

// spread: shallow copy — nested masih ditunjuk bersama
const salinanSpread = { ...profil };
salinanSpread.nama = "Budi S.";                      // aman: properti atas terpisah
salinanSpread.kontak.email = "[email protected]";   // nested ikut terdampak

console.log("Spread - nama profil :", profil.nama);
console.log("Spread - email profil:", profil.kontak.email);

// structuredClone: deep copy — nested benar-benar terpisah
const klon = structuredClone(profil);
klon.kontak.email = "[email protected]";
console.log("Clone - email profil :", profil.kontak.email);
console.log("Clone - email klon   :", klon.kontak.email);

// structuredClone menangani Date dengan benar
const dokumen = {
  judul: "Laporan Kuartal 1",
  dibuat: new Date(2026, 0, 15)
};
const klonDokumen = structuredClone(dokumen);
klonDokumen.dibuat.setFullYear(2030);
console.log("Tanggal asli :", dokumen.dibuat.getFullYear());
console.log("Tanggal klon :", klonDokumen.dibuat.getFullYear());

uji-referensi.js

Unduh berkas
javascript
// uji-referensi.js
// Object menyimpan referensi, bukan salinan nilai.

const keranjangA = { total: 150000, item: 3 };
const keranjangB = keranjangA;               // referensi yang sama

console.log(keranjangA === keranjangB);      // true: satu object, dua nama

keranjangB.total = 175000;
console.log("A total:", keranjangA.total);   // ikut berubah
console.log("B total:", keranjangB.total);

// object terpisah, walaupun isinya sama persis
const keranjangC = { total: 175000, item: 3 };
console.log(keranjangA === keranjangC);      // false

// perbandingan manual: kunci dan nilai dibandingkan satu per satu
function samaKonten(a, b) {
  const kunciA = Object.keys(a);
  const kunciB = Object.keys(b);
  if (kunciA.length !== kunciB.length) {
    return false;
  }
  for (let i = 0; i < kunciA.length; i++) {
    if (a[kunciA[i]] !== b[kunciA[i]]) {
      return false;
    }
  }
  return true;
}

console.log(samaKonten(keranjangA, keranjangC)); // true: kontennya identik
console.log(samaKonten(keranjangA, keranjangB)); // true: memang referensi sama