Budi menata menu kafe dalam satu array, lalu “menyalinnya” dengan assignment biasa: const menuEtalase = menuPagi. Malam itu menu etalase diperbarui satu item, dan esoknya menu pagi ikut berubah — padahal tidak ada satu pun kode yang menyentuhnya. Yang terjadi bukan keanehan engine, melainkan cara kerja referensi yang kita bongkar di section Objects: assignment tidak menggandakan data, yang digandakan hanya jalannya, sehingga dua variabel itu menunjuk satu array yang sama.
Immutability adalah keterampilan menghindari situasi tersebut: data tidak diubah di tempatnya, melainkan diperbarui lewat salinan. Setiap perubahan menghasilkan data baru dan membiarkan yang lama utuh, sehingga bagian program lain yang masih memegang data lama tetap membaca nilai yang benar. Chapter ini membuktikan masalah mutasinya, lalu mempraktikkan empat alat salin JavaScript — spread, method salinan ES2023, Object.freeze, dan pola pembaruan object bersarang.
Mutasi: Satu Perubahan, Banyak Tempat #
Berkas berikut memuat lima eksperimen berurutan. Eksperimen pertama sengaja mengulang insiden Budi — dua nama untuk satu array, satu push, dua tempat yang berubah:
// data-tanpa-mutasi.js
// Mutasi vs salinan: menjaga data asal tetap utuh.
// --- 1. dua nama untuk satu array: mutasi terlihat dari keduanya ---
const menuPagi = ["kopi susu", "teh tarik", "roti bakar"];
const menuEtalase = menuPagi;
menuEtalase.push("pisang goreng");
console.log(menuPagi.length);
console.log(menuEtalase.length);
// --- 2. salinan spread: array yang berdiri sendiri ---
const menuSore = [...menuPagi, "es kopi"];
console.log(menuSore);
menuSore.push("kue cubit");
console.log(menuPagi.length);
console.log(menuSore.length);
// --- 3. method salinan: toSorted dan with ---
const hargaKopi = [18000, 12000, 24000];
const hargaTerurut = hargaKopi.toSorted((a, b) => a - b);
const hargaRevisi = hargaKopi.with(1, 15000);
console.log(hargaTerurut);
console.log(hargaRevisi);
console.log(hargaKopi);
// --- 4. Object.freeze: beku hanya di permukaan (shallow) ---
const resep = Object.freeze({
nama: "Kopi Susu",
bahan: ["espresso", "susu"],
});
// resep.nama = "Kopi Gula Aren"; // di script biasa: gagal senyap
resep.bahan.push("gula aren"); // array di dalamnya tidak ikut beku
console.log(resep.bahan);
console.log(Object.isFrozen(resep));
console.log(Object.isFrozen(resep.bahan));
// --- 5. memperbarui object nested tanpa mutasi ---
const pesanan = {
pemesan: "Budi",
detail: { item: "Kopi Susu", jumlah: 2 },
};
const pesananBaru = {
...pesanan,
detail: { ...pesanan.detail, jumlah: 3 },
};
console.log(pesanan.detail.jumlah);
console.log(pesananBaru.detail.jumlah);
console.log(pesananBaru.pemesan);Output yang Dihasilkan:
4
4
[ 'kopi susu', 'teh tarik', 'roti bakar', 'pisang goreng', 'es kopi' ]
4
6
[ 12000, 18000, 24000 ]
[ 18000, 15000, 24000 ]
[ 18000, 12000, 24000 ]
[ 'espresso', 'susu', 'gula aren' ]
true
false
2
3
BudiMari kita telaah baris-baris pentingnya:
- Baris 5–10 (dua nama, satu array):
menuEtalase = menuPagitidak membuat array kedua.pushdi baris 8 menambah elemen pada satu-satunya array itu, sehinggamenuPagi.lengthdi baris 9 ikut membaca4— mutasi terlihat dari dua nama sekaligus. - Baris 13 (salinan spread):
[...menuPagi, "es kopi"]membongkar isimenuPagike dalam array literal baru, lalu menambah"es kopi"di ujungnya. Yang dihasilkan array berdiri sendiri dengan lima elemen. - Baris 16–18 (bukti kemandirian):
pushkini menyasarmenuSoresaja —menuPagi.lengthtetap4, sedangkanmenuSore.lengthnaik menjadi6. Dua array tidak saling menjangkau lagi. - Baris 22–23 (method salinan):
toSortedmengembalikan salinan yang terurut menaik, danwith(1, 15000)mengembalikan salinan dengan elemen index1diganti. Keduanya tidak menyentuh array pemanggilnya — baris 26 menegaskanhargaKopimasih[18000, 12000, 24000]. - Baris 29–38 (freeze shallow):
Object.freezemengunci properti milikresep— assignment di baris 34 yang sengaja kami beri komentar akan gagal senyap di script biasa. Tetapibahandi dalamnya tetap bisa di-pushdi baris 35:Object.isFrozen(resep)menjawabtrue, sedangkanObject.isFrozen(resep.bahan)menjawabfalse. - Baris 41–49 (pembaruan nested):
pesananBarudibangun dari dua lapis salinan — spreadpesananuntuk lapis luar, lalu spreadpesanan.detailuntuk lapis dalam denganjumlahbaru.pesananlama tetap2,pesananBarumembaca3, dan propertipemesanikut terbawa karena lapis luarnya disalin utuh.
Method Salinan: Pasangan Bebas Mutasi #
Eksperimen ketiga memperlihatkan kebiasaan modern JavaScript: untuk hampir setiap method yang bermutasi, tersedia pasangan yang mengembalikan salinan. sort punya toSorted, reverse punya toReversed, dan mengganti satu elemen tanpa mutasi cukup with(index, nilaiBaru):
const antrean = ["Budi", "Siti", "Dewi"];
console.log(antrean.toReversed());
console.log(antrean.with(0, "Adi"));
console.log(antrean);Output yang Dihasilkan:
[ 'Dewi', 'Siti', 'Budi' ]
[ 'Adi', 'Siti', 'Dewi' ]
[ 'Budi', 'Siti', 'Dewi' ]Ketiga baris keluaran memperlihatkan pola yang sama: hasil baru muncul, array asal tidak bergeser sedikit pun. Detail comparator toSorted dan aturan pemakaiannya sudah dibahas tuntas di chapter Sorting dan Reversing section Arrays; di sini yang penting kebiasaannya — ketika data asal masih dipakai bagian lain program, pilihlah varian salin.
Sebagai rangkuman, empat alat salin yang sudah kita pegang bisa dipetakan ke kebutuhannya masing-masing:
| Kebutuhan | Alat | Catatan |
|---|---|---|
| Salin array atau object satu lapis | spread [...x], {...x} | lapis dalam tetap dirujuk bersama |
| Variasi urutan atau satu elemen | toSorted, toReversed, with | pasangan bebas mutasi dari sort, reverse, index |
| Salinan utuh sampai lapis terdalam | structuredClone | object baru seutuhnya, termasuk Date |
| Kunci data agar tidak bisa diubah | Object.freeze | hanya satu tingkat; nested tetap terbuka |
Object.freeze: Beku Hanya di Permukaan #
Object.freeze dari chapter Object Methods mengunci properti satu tingkat saja — sifat inilah yang disebut shallow. Eksperimen keempat membuktikannya dua arah: resep benar-benar terkunci, tetapi array bahan yang menjadi isinya tetap terbuka untuk push. Pembacaan Object.isFrozen(resep.bahan) yang bernilai false adalah kesimpulan paling ringkas: freeze tidak menelusuri ke dalam.
Pola Pembaruan Object Bersarang #
Eksperimen kelima memperlihatkan pola yang paling sering dipakai dalam kerja sehari-hari. Aturannya satu kalimat: setiap lapis yang berubah harus disalin; lapis yang tidak berubah boleh dirujuk ulang. Pada pesananBaru, yang berubah hanya detail.jumlah, maka hanya detail yang disalin ulang — dan pemesan tidak perlu disalin karena nilainya tak tersentuh.
const titipan = { pemesan: "Siti", detail: { item: "Roti Bakar", jumlah: 1 } };
const titipanBaru = { ...titipan, detail: { ...titipan.detail, jumlah: 4 } };
console.log(titipan.detail.jumlah);
console.log(titipanBaru.detail.jumlah);Output yang Dihasilkan:
1
4Pola ini terlihat bertele-tele dibanding titipan.detail.jumlah = 4, tetapi perbedaannya bukan soal gaya: versi mutasi membuat titipan lama ikut bernilai 4, sedangkan versi salinan membuat dua keadaan yang bisa dibaca bersamaan. Itulah alasan pola ini menjadi bahasa sehari-hari di framework frontend yang membandingkan keadaan lama dan baru.
Satu kekhawatiran yang sering diajukan: apakah menyalin setiap perubahan tidak mahal? Jawabannya, salinan spread bekerja secara dangkal — array baru hanya menyimpan referensi ke elemen yang sama. Pada pesananBaru, yang benar-benar baru hanyalah dua object: lapis luarnya dan lapis detail. String dan angka tidak digandakan sama sekali, sehingga biayanya sebanding dengan ukuran lapisan yang berubah, bukan ukuran seluruh data.
Freeze Berlapis untuk Data yang Benar-benar Terkunci #
Bila data konfigurasi memang harus terkunci sampai lapis terdalam, freeze bisa dipanggil berulang lewat rekursi — konsep dari chapter Recursion:
function bekukanDalam(nilai) {
Object.freeze(nilai);
for (const bagian of Object.values(nilai)) {
if (typeof bagian === "object" && bagian !== null) {
bekukanDalam(bagian);
}
}
}
const konfigurasi = { kantor: { kota: "Bandung", jadwal: ["pagi", "sore"] } };
bekukanDalam(konfigurasi);
console.log(Object.isFrozen(konfigurasi));
console.log(Object.isFrozen(konfigurasi.kantor));
console.log(Object.isFrozen(konfigurasi.kantor.jadwal));Output yang Dihasilkan:
true
true
trueObject.values menelusuri setiap properti, dan function memanggil dirinya sendiri bila nilainya object atau array — sehingga array jadwal ikut dibekukan. Ketiga pemeriksaan menjawab true. Pola ini dipakai untuk data yang memang tidak pernah ditulis ulang selama program berjalan; untuk data yang rutin diperbarui, pola salinan pada bagian sebelumnya tetap pilihan yang tepat.
Dengan data yang tidak berubah di tempatnya, pemrosesan bisa dirangkai tanpa saling menimpa. Alat rangkaiannya adalah function yang menerima function — topik chapter berikutnya, higher-order functions.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
data-tanpa-mutasi.js
Unduh berkas// data-tanpa-mutasi.js
// Mutasi vs salinan: menjaga data asal tetap utuh.
// --- 1. dua nama untuk satu array: mutasi terlihat dari keduanya ---
const menuPagi = ["kopi susu", "teh tarik", "roti bakar"];
const menuEtalase = menuPagi;
menuEtalase.push("pisang goreng");
console.log(menuPagi.length);
console.log(menuEtalase.length);
// --- 2. salinan spread: array yang berdiri sendiri ---
const menuSore = [...menuPagi, "es kopi"];
console.log(menuSore);
menuSore.push("kue cubit");
console.log(menuPagi.length);
console.log(menuSore.length);
// --- 3. method salinan: toSorted dan with ---
const hargaKopi = [18000, 12000, 24000];
const hargaTerurut = hargaKopi.toSorted((a, b) => a - b);
const hargaRevisi = hargaKopi.with(1, 15000);
console.log(hargaTerurut);
console.log(hargaRevisi);
console.log(hargaKopi);
// --- 4. Object.freeze: beku hanya di permukaan (shallow) ---
const resep = Object.freeze({
nama: "Kopi Susu",
bahan: ["espresso", "susu"],
});
// resep.nama = "Kopi Gula Aren"; // di script biasa: gagal senyap
resep.bahan.push("gula aren"); // array di dalamnya tidak ikut beku
console.log(resep.bahan);
console.log(Object.isFrozen(resep));
console.log(Object.isFrozen(resep.bahan));
// --- 5. memperbarui object nested tanpa mutasi ---
const pesanan = {
pemesan: "Budi",
detail: { item: "Kopi Susu", jumlah: 2 },
};
const pesananBaru = {
...pesanan,
detail: { ...pesanan.detail, jumlah: 3 },
};
console.log(pesanan.detail.jumlah);
console.log(pesananBaru.detail.jumlah);
console.log(pesananBaru.pemesan);