Referensi vs nilai

Perilaku penyalinan primitif vs object dan bug mutasi yang menyertainya.

Daftar isi

Menyalin data di JavaScript mengikuti dua aturan yang berbeda, dan perbedaannya sering menjadi sumber bug yang sulit dilacak. Nilai primitif seperti angka dan string disalin isinya, sedangkan object dan array disalin referensi, yaitu penunjuk ke data yang sama di memori. Topik ini membahas kedua aturan itu, perbandingan dua variabel, bug mutasi yang muncul lewat argumen fungsi, serta cara aman menyalin data sebelum mengubahnya.

Primitif disalin nilainya#

Primitif adalah nilai dasar seperti number, string, boolean, null, dan undefined, dan daftar lengkapnya sudah dibahas pada topik tipe data. Penugasan satu primitif ke variabel lain menyalin nilainya secara utuh, sehingga kedua variabel kemudian berjalan sendiri-sendiri. Mengubah salah satunya tidak pernah memengaruhi yang lain, berapa pun salinannya dibuat.

js Contoh: primitif disalin nilainya
// Primitif disalin nilainya: dua variabel saling lepas.
let nilaiA = 70;
let nilaiB = nilaiA;

nilaiB = nilaiB + 10;

console.log(nilaiA);
console.log(nilaiB);
text Keluaran
70
80

nilaiB lahir sebagai salinan 70, lalu dinaikkan menjadi 80, dan nilaiA tetap 70. Keterpisahan ini berlaku untuk semua primitif tanpa kecuali, dan bukan sekadar tampak terpisah, melainkan benar-benar tidak berbagi apa pun. Tidak ada keterkaitan yang bisa bocor seberapa pun banyak salinan dibuat, karena setiap penugasan menduplikasi nilainya secara utuh. Akibatnya, membandingkan dua primitif dengan === membandingkan nilainya, sehingga 70 === 70 selalu true tanpa peduli asal variabelnya. Kondisi ini berubah pada object dan array, dan perbedaannya perlu dipahami dengan baik sebelum menulis fungsi yang menerima data.

Object dan array disalin referensinya#

Penugasan object dan array tidak menyalin isinya, melainkan menyalin referensinya. Dua variabel yang menerima referensi sama membaca dan mengubah satu data yang persis sama. Perbandingan === pada dua variabel pun menguji referensinya, bukan kesamaan isi.

js Contoh: perbandingan referensi dua variabel
// Perbandingan object membandingkan referensinya, bukan isinya.
const rakA = { kode: "R-01" };
const rakB = rakA;
const rakC = { kode: "R-01" };

console.log(rakA === rakB);
console.log(rakA === rakC);

rakB.kode = "R-02";
console.log(rakA.kode);
text Keluaran
true
false
R-02

rakA dan rakB menunjuk ke object yang sama, sehingga === menghasilkan true dan perubahan kode lewat rakB terbaca juga lewat rakA. Sebaliknya, rakC ditulis dengan isi yang persis sama, tetapi === tetap false karena ia object lain yang berdiri sendiri. Diagram berikut merangkum hubungannya, dua variabel dan satu data.

variabelrakAvariabelrakBsalinan referensisalinan referensiobject{ kode: "R-01" }hanya satu di memori
Penugasan menyalin referensi, sehingga rakA dan rakB menunjuk ke satu object yang sama dan perubahan lewat salah satunya terlihat oleh yang lain

Pemahaman ini menjelaskan banyak perilaku yang sebelumnya tampak aneh, misalnya array ikut berubah padahal yang disalin hanya variabelnya. Data yang dibagikan lewat referensi tidak berbahaya selama semua pihak menyadari pembagiannya. Bahayanya baru muncul ketika satu pihak menganggap data itu miliknya sendiri lalu mengubah isinya, dan itulah inti bug mutasi pada bagian ini.

Bug mutasi lewat argumen fungsi#

Perubahan isi data disebut mutasi, yakni penyuntingan nilai di tempat asalnya tanpa membuat data baru. Mutasi lewat referensi adalah pola bug yang paling umum pada data JavaScript. Fungsi menerima array sebagai argumen, lalu mengubah isinya dengan keyakinan bahwa array itu miliknya sendiri, padahal pemanggil masih memakai array yang sama untuk keperluan lain.

js bug-mutasi.js
// Fungsi mengubah array argumennya, dan array asli ikut berubah.
function naikkanSemua(nilai, tambahan) {
  for (let i = 0; i < nilai.length; i++) {
    nilai[i] = nilai[i] + tambahan;
  }
  return nilai;
}

const nilaiAsli = [70, 80, 90];
const hasil = naikkanSemua(nilaiAsli, 5);

console.log(hasil);
console.log(nilaiAsli);
console.log(hasil === nilaiAsli);
text Keluaran
[ 75, 85, 95 ]
[ 75, 85, 95 ]
true

Nilai balik fungsi memang sesuai harapan, [75, 85, 95], tetapi nilaiAsli ikut berubah menjadi nilai yang sama, dan baris terakhir menegaskan bahwa keduanya adalah array yang sama. Fungsi naikkanSemua tidak berusaha merusak apa pun, ia hanya menulis ke array yang diterimanya, dan justru itulah masalahnya. Pemanggil yang tidak membaca isi fungsi tidak akan menduga datanya berubah begitu fungsi selesai. Bug semacam ini jarang terlihat pada percobaan kecil, karena data yang sudah termutasi biasanya tidak dipakai lagi setelah pemanggilan. Masalahnya baru muncul ketika fungsi yang sama dipanggil lagi dengan data yang masih dibutuhkan.

Menyalin dulu sebelum mengubah#

Cara aman menghadapi perilaku ini adalah menyalin data sebelum mengubahnya. Spread menyalin array dan object satu tingkat dengan cepat, sedangkan map dan filter selalu menghasilkan array baru sehingga aman dipakai pada data asli. Setelah salinan dibuat, mutasi hanya menyentuh salinannya, dan data asli tetap utuh untuk pemakai lain.

js Contoh: menyalin dengan spread dan map sebelum mengubah
// Salin dulu dengan spread, atau hasilkan array baru dengan map.
function naikkanSemua(nilai, tambahan) {
  for (let i = 0; i < nilai.length; i++) {
    nilai[i] = nilai[i] + tambahan;
  }
  return nilai;
}

const nilaiAsli = [70, 80, 90];
const hasilAman = naikkanSemua([...nilaiAsli], 5);
const hasilMap = nilaiAsli.map((n) => n + 5);

console.log(hasilAman);
console.log(hasilMap);
console.log(nilaiAsli);
text Keluaran
[ 75, 85, 95 ]
[ 75, 85, 95 ]
[ 70, 80, 90 ]

Kedua hasil bernilai sama seperti sebelumnya, tetapi nilaiAsli kali ini tetap [70, 80, 90]. Salinan [...nilaiAsli] memutus hubungan referensi pada tingkat terluar, sedangkan map membangun array baru tanpa menyentuh asalnya. Pilihan alatnya menyesuaikan bentuk pekerjaannya, spread untuk penyalinan sederhana, dan map atau filter untuk transformasi yang sekaligus menghasilkan array baru. Pengaman lain adalah Object.freeze, yang membekukan object sehingga penambahan, pengubahan, dan penghapusan propertinya tidak berpengaruh. Pembekuan ini tidak menyalin apa pun, dan object yang sama tetap dibagikan.

Ringkasan#

  • Primitif disalin nilainya sehingga dua variabel saling lepas, sedangkan object dan array disalin referensinya sehingga dua variabel berbagi satu data.
  • === pada object dan array membandingkan referensi, dua literal dengan isi sama tetap berbeda selama alamatnya berbeda.
  • Fungsi yang menulis ke argumennya memutasi data pemanggil, dan itulah pola bug mutasi yang paling umum.
  • Pencegahannya adalah menyalin dengan spread atau menghasilkan array baru dengan map dan filter sebelum mengubah, ditambah Object.freeze untuk data yang wajib kebal mutasi.

Referensi#