Program bekerja dengan cara menyimpan nilai lalu membacanya kembali di baris-baris berikutnya, dan variabel adalah mekanisme JavaScript untuk melakukan hal tersebut. Kualitas kode Node.js sangat dipengaruhi cara kita mendeklarasikan variabel: kata kunci yang tepat membuat niat kode terbaca jelas dan menyimpan kita dari bug scope yang sulit dilacak. Sejak ES 2015, JavaScript memiliki tiga kata kunci deklarasi — var, let, dan const — dengan aturan scope yang berbeda. Pada chapter ini kita akan membedah ketiganya, memahami block scope, mengupas hoisting, sampai mengenali jebakan temporal dead zone yang sering membingungkan pemula.
Aturan Deklarasi Variabel #
Ketiga kata kunci deklarasi punya perbedaan scope dan perilaku penugasan ulang. Tabel berikut merangkum perbedaan pokoknya:
| Kata kunci | Scope | Bisa di-assign ulang | Perilaku hoisting |
|---|---|---|---|
var | function scope | ya | diangkat, berisi undefined |
let | block scope | ya | diangkat, tapi terkunci temporal dead zone |
const | block scope | tidak | diangkat, tapi terkunci temporal dead zone |
Kode modern Node.js memakai const sebagai pilihan bawaan, beralih ke let hanya ketika nilai memang perlu diubah, dan meninggalkan var sepenuhnya. Alasannya sederhana: const menjamin binding antara nama variabel dan nilai tidak berganti, sehingga pembaca kode bisa percaya bahwa nama di baris ke-50 masih menunjuk nilai yang sama seperti di baris pertama. Simpan contoh berikut sebagai scope-dasar.mjs, lalu jalankan dengan node scope-dasar.mjs:
const nama = "Budi";
let skor = 10;
skor = skor + 5;
console.log(`${nama} memiliki skor ${skor}`);
// const menolak penugasan ulang nama variable yang sama
try {
nama = "Siti";
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
// const mengunci referensi, bukan isi object
const pengaturan = { tema: "gelap", bahasa: "id" };
pengaturan.tema = "terang";
console.log(`Tema saat ini: ${pengaturan.tema}`);Output yang Dihasilkan:
Budi memiliki skor 15
TypeError: Assignment to constant variable.
Tema saat ini: terangMari kita telaah baris-baris kode di atas:
- Baris 1-2 (
const nama,let skor):namadikunci denganconstkarena identitas pengguna tidak berubah di sepanjang program, sedangkanskormemakailetkarena nilainya memang akan diperbarui. - Baris 4 (
skor = skor + 5): penugasan ulang padaletberjalan normal; hasilnya15. - Baris 9-12 (
try { nama = "Siti" }): menugaskan ulangconstmelemparTypeError. Bloktry...catchmenangkap error tersebut agar program tidak berhenti, lalu mencetake.namedane.message— pola cetak error yang akan kita pakai berulang kali di Course ini. - Baris 16-18 (
pengaturan.tema = "terang"):constmengunci referensi ke object, bukan isi object-nya. Property di dalampengaturantetap boleh diubah.
Block Scope dan Batas Visibilitas #
Block scope adalah wilayah hidup variabel yang dibatasi oleh pasangan kurung kurawal {} — milik blok if, for, while, atau blok mandiri. Saat JavaScript engine menemui deklarasi let atau const di dalam blok, engine mencatat variabel tersebut sebagai anggota blok dan menghapusnya begitu blok selesai dieksekusi. Kata kunci var mengabaikan aturan ini karena var hanya mengenal function scope: deklarasi var di dalam blok apa pun akan menempel pada function yang sedang berjalan, atau pada scope global bila tidak ada function. Perhatikan perbedaannya pada block-scope.mjs:
// var hidup di function scope, bukan block scope
if (true) {
var laporanVar = "tersimpan di function scope";
let laporanLet = "tersimpan di block scope";
console.log(`Di dalam blok -> var: ${laporanVar}, let: ${laporanLet}`);
}
console.log(`Di luar blok -> var masih terlihat: ${laporanVar}`);
// let tidak lagi dikenal di luar blok if di atas
try {
console.log(laporanLet);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}Output yang Dihasilkan:
Di dalam blok -> var: tersimpan di function scope, let: tersimpan di block scope
Di luar blok -> var masih terlihat: tersimpan di function scope
ReferenceError: laporanLet is not definedUrutan eksekusinya sebagai berikut:
- Baris 3-4: kedua variabel dideklarasikan di dalam blok
if. Di dalam blok, keduanya terlihat normal. - Baris 7: setelah blok berakhir,
laporanVarmasih bisa dibaca karenavarmenempel pada scope global modul, bukan pada blokif. - Baris 10-13: membaca
laporanLetdi luar blok melemparReferenceErrorkarena variabel tersebut sudah tidak ada sama sekali pada scope ini.
Perilaku per-iterasi let di dalam loop memberi efek yang sangat praktis. Setiap iterasi for mendapat salinan i sendiri, sehingga function yang disimpan di dalam loop tetap mengingat nilai iterasinya:
function denganVar() {
const hasil = [];
for (var i = 1; i <= 3; i++) hasil.push(function () { return i; });
return hasil.map(function (f) { return f(); }).join(", ");
}
function denganLet() {
const hasil = [];
for (let i = 1; i <= 3; i++) hasil.push(function () { return i; });
return hasil.map(function (f) { return f(); }).join(", ");
}
console.log(`var: ${denganVar()}`);
console.log(`let: ${denganLet()}`);Output yang Dihasilkan:
var: 4, 4, 4
let: 1, 2, 3Pada denganVar, seluruh function mengembalikan 4 karena ketiga function menunjuk variabel i yang sama — satu-satunya i di function scope — yang sudah bernilai 4 ketika loop selesai. Pada denganLet, setiap iterasi membuat binding i baru, jadi masing-masing function menyimpan nilai 1, 2, dan 3 dari iterasi saat function dibuat.
Hoisting dan Temporal Dead Zone #
Hoisting adalah perilaku JavaScript engine yang membaca seluruh deklarasi sebuah scope sebelum mengeksekusi baris demi baris. Untuk var, hasilnya seperti deklarasi dipindahkan ke atas scope dengan nilai undefined; untuk function declaration, seluruh isi function sudah bisa dipanggil sebelum baris deklarasinya. Deklarasi let dan const juga diangkat, tetapi dengan perbedaan penting: engine menandai variabel tersebut berada di temporal dead zone (TDZ) — wilayah antara awal scope dan baris deklarasi — dan melempar ReferenceError bila variabel diakses sebelum waktunya. Kita bisa menyaksikan ketiganya dalam hoisting-tdz.mjs:
// Hoisting var: deklarasi diangkat ke atas, nilainya masih undefined
console.log(`typeof kota -> ${typeof kota}`);
var kota = "Bandung";
// Function declaration bisa dipanggil sebelum baris deklarasinya
sapa("Budi");
function sapa(nama) {
console.log(`Halo, ${nama}!`);
}
// Temporal dead zone: let sudah dikenal engine, tapi belum boleh diakses
try {
console.log(nilaiAwal);
let nilaiAwal = 100;
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}Output yang Dihasilkan:
typeof kota -> undefined
Halo, Budi!
ReferenceError: Cannot access 'nilaiAwal' before initializationMari kita baca hasilnya satu per satu:
- Baris 2-3:
typeof kotamenghasilkan"undefined", bukan error, karena hoistingvarsudah mendaftarkankotasejak awal scope — hanya nilainya belum terisi. - Baris 7:
sapa("Budi")berhasil dipanggil meskipun barisfunction sapaberada di bawahnya; function declaration diangkat beserta isinya. - Baris 15-18: mengakses
nilaiAwalsebelum barislet nilaiAwalmelemparReferenceErrordengan pesan spesifikCannot access 'nilaiAwal' before initialization— tanda khas TDZ.
Konvensi Penulisan di Node.js #
Di berkas .mjs, setiap berkas adalah module dengan scope sendiri — variabel level atas tidak menjadi property global dan tidak bocor ke berkas lain, apa pun kata kunci yang dipakai. Konvensi yang kita pakai sepanjang Course ini: mulai dengan const, naikkan ke let hanya setelah kompilator membuktikan kebutuhan penugasan ulang, dan jangan menulis var baru lagi. Gaya ini juga yang dipakai dokumentasi resmi Node.js, misalnya const server = http.createServer(...) yang akan sering kita tulis pada Section HTTP dan Web Servers.
Referensi lanjutan untuk topik chapter ini:
Deklarasi variabel kini jelas: kita tahu kapan memakai const, mengapa var berbahaya, dan bagaimana engine memperlakukan hoisting. Variabel hanyalah wadah — hal berikutnya yang perlu kita kuasai adalah nilai apa saja yang bisa dimasukkan ke dalamnya, karena JavaScript mengelompokkan nilai ke dalam tipe data dengan aturan konversi yang unik. Mari lanjut ke Chapter Tipe Data dan Konversi.