Belajar dengan membaca secara berurutan

Scope dan Hoisting — Dari Mana dan Kapan Variabel Bisa Dibaca

Scope menentukan dari mana variabel bisa dibaca, dan hoisting mendaftarkan deklarasi sebelum kode berjalan. Termasuk shadowing, TDZ, dan alasan tidak memakai var.

Value yang dihitung di dalam kurung kurawal tidak bisa dipakai dari luar kurung kurawal itu — eksekusinya langsung berhenti. Hal yang sama terjadi saat sebuah baris membaca nilai pengaturan yang baru dideklarasikan di baris bawahnya. Tanpa memahami aturannya, sulit menebak dari mana dan mulai kapan sebuah nama bisa dipakai.

Artikel ini membahas scope, yang menentukan dari mana nama yang sudah dideklarasikan bisa dibaca, dan hoisting, yang mendaftarkan deklarasi sebelum kode dijalankan.

Variabel yang Hanya Bisa Dipakai di Dalam Kurung Kurawal — Block Scope

Misalnya kamu ingin menampilkan di halaman keranjang berapa yen lagi yang perlu dibelanjakan supaya dapat gratis ongkos kirim. Selisihnya bisa dihitung di dalam if yang mengecek gratis ongkos kirim, tapi kalau value itu ditampilkan di luar kurung kurawalnya, eksekusi berhenti dengan ReferenceError.

Scope (batas yang menentukan dari mana sebuah nama bisa dibaca) dibatasi oleh kurung kurawal. Batas satu pasang kurung kurawal disebut block scope, dan variabel yang dideklarasikan dengan const atau let hanya bisa dibaca di dalam block scope terdalam yang membungkus deklarasinya.

const cartTotal = 4800;            // Level terluar, tidak di dalam kurung kurawal mana pun

function checkShipping(total) {
  const freeLine = 5000;           // Hanya di dalam kurung kurawal checkShipping

  if (total < freeLine) {
    const shortage = freeLine - total;  // Hanya di dalam kurung kurawal if
    console.log(`Kurang ${shortage} yen lagi`);  // Kurang 200 yen lagi
  }

  // Variabel yang dideklarasikan di luar bisa dibaca dari dalam
  console.log(cartTotal);          // 4800
  // console.log(shortage);        // ReferenceError: shortage is not defined
}

checkShipping(cartTotal);
Variabel yang Terbaca dari Luar atau Tidak
Level terluar (tidak di dalam kurung kurawal mana pun)
  • const cartTotal = 4800 — bisa dibaca dari mana saja di file
  • checkShipping(cartTotal) — memanggil function dari level terluar
Kurung kurawal checkShipping
  • const freeLine = 5000 — hanya bisa dibaca di dalam kurung kurawal ini
  • cartTotal dari luar tetap bisa dibaca
  • console.log(shortage) — tidak terbaca setelah keluar dari if, jadi ReferenceError
Kurung kurawal if
  • const shortage = 200 — hanya bisa dibaca di dalam kurung kurawal ini
  • freeLine dan cartTotal sama-sama bisa dibaca
Kode di dalam bisa membaca value dari luar, tapi value yang dibuat di dalam if tidak bisa dibaca lagi begitu keluar dari kurung kurawalnya. Arah membacanya cuma satu: dari dalam ke luar.

Level terluar yang tidak dibungkus kurung kurawal apa pun adalah global scope, dan variabel di sana bisa dibaca dari mana saja. Body function juga diapit kurung kurawal, jadi freeLine tidak bisa dibaca dari luar function. Batas yang dibentuk kurung kurawal function disebut function scope.

Tampilkan sisa poin yang dibutuhkan member untuk naik ke level berikutnya. points dan nextRankLine sudah dideklarasikan.

① Deklarasikan remaining dengan let untuk menyimpan sisa poin, di posisi yang bisa dibaca dari luar if.

② Hanya saat poinnya belum cukup, isi remaining dengan sisa poin di dalam kurung kurawal if.

③ Di luar if, tampilkan dalam bentuk "Kurang N poin lagi ke level berikutnya".

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Deklarasi di Dalam Didahulukan — Shadowing

Saat menambahkan function yang menghitung harga dengan tarif pajak yang lebih rendah, bisa saja kamu mendeklarasikan variabel taxRate di dalamnya, padahal nama itu sudah dipakai variabel di luar. Kalau kamu tidak tahu value mana yang dipakai, kamu tidak bisa melacak kenapa hasil hitungannya meleset.

Kalau di scope dalam kamu mendeklarasikan nama yang sama dengan nama di scope luar, di dalam kurung kurawal itu yang dipakai adalah deklarasi dalam. Ini disebut shadowing (deklarasi di dalam menutupi variabel di luar yang namanya sama), dan terjadi baik di kurung kurawal if maupun kurung kurawal function.

const taxRate = 0.1;                 // Tarif pajak yang ditetapkan di luar

function withTax(price) {
  return Math.floor(price * (1 + taxRate));
}

function withReducedTax(price) {
  const taxRate = 0.08;              // Nama yang sama, dideklarasikan di dalam function
  return Math.floor(price * (1 + taxRate));
}

console.log(withTax(1000));          // 1100
console.log(withReducedTax(1000));   // 1080
console.log(taxRate);                // 0.1 (yang di luar tidak berubah)
Nama Sama, Value Beda Tergantung Tempat Membacanya
const taxRate= 0.1 (luar)Dibaca didalam withTaxDibaca di dalamwithReducedTaxDibaca diluar functionTanpa deklarasidi dalam: 0.1Menemukan0.08 di dalamMemakai 0.1dari luarMengembalikan1100Mengembalikan1080Menampilkan 0.1
Nama taxRate yang sama menemukan deklarasi yang berbeda tergantung dari mana kamu membacanya. Deklarasi di dalam tidak mengubah value di luar.

Nama dicari mulai dari kurung kurawal terdalam yang membungkus baris yang membacanya. Di dalam withReducedTax, pencarian berhenti begitu 0.08 ditemukan, jadi 0.1 di luar tidak pernah dibaca. Satu nama yang berisi value berbeda di tiap function membingungkan pembaca kode. Di kode kamu sendiri, pakai nama yang berbeda, misalnya reducedTaxRate.

Tanpa const, yang Di-assign Justru Variabel di Luar

Kalau kamu menulis taxRate = 0.08; di dalam function tanpa const, tidak ada variabel baru yang dibuat — baris itu justru meng-assign taxRate di luar. Kalau yang di luar adalah const, eksekusi berhenti dengan TypeError, tapi kalau let, value-nya tertimpa, dan pemanggilan withTax(1000) setelahnya pun ikut mengembalikan 1080.

Ganti judul harga di halaman produk, tapi hanya selama promo. label dan isSale sudah dideklarasikan.

① Saat isSale bernilai true, deklarasikan label sebagai "Harga promo" di dalam kurung kurawal if, lalu tampilkan.

② Tampilkan label di luar if.

③ Tulis if kedua dengan nama di dalamnya diganti menjadi saleLabel, lalu tampilkan dalam bentuk "Harga normal → Harga promo".

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Tidak Bisa Dibaca Sebelum Baris Deklarasinya — Hoisting dan TDZ

Misalnya kamu ingin mengumpulkan nilai pengaturan seperti persentase poin atau ongkos kirim di baris-baris bawah, setelah kode yang memakainya. Seperti yang sudah kamu lihat di artikel tentang function, deklarasi function tetap bisa dipanggil walaupun ditulis di bawah, tapi membaca nilai pengaturan let atau const sebelum baris deklarasinya akan menghentikan eksekusi dengan ReferenceError.

Sebelum mulai menjalankan sebuah scope, JavaScript lebih dulu mendaftarkan nama-nama deklarasi di dalamnya. Ini disebut hoisting (deklarasi didaftarkan sebelum kode dijalankan). Deklarasi function mendaftarkan nama dan body-nya sekaligus, jadi bisa dipanggil dari baris di atas deklarasinya. let dan const hanya mendaftarkan namanya; value-nya baru masuk saat baris deklarasi dijalankan.

// Body function membaca pointRate saat function dipanggil
function calcPoint(amount) {
  return Math.floor(amount * pointRate);
}

// Kalau dipanggil di sini, baris deklarasi pointRate belum dijalankan
// console.log(calcPoint(3200));
// → ReferenceError: Cannot access 'pointRate' before initialization

let pointRate = 0.05;              // Nilai pengaturan dikumpulkan di bawah

console.log(calcPoint(3200));      // 160 (bisa dibaca karena baris deklarasinya sudah dijalankan)
Kapan Value let Mulai Bisa Dibaca
Scope mulaidijalankanHanya namapointRate yangterdaftarSelama baris letbelum tercapaiPanggil calcPoint→ ReferenceErrorMenjalankanlet pointRate= 0.05Pemanggilansetelahnyamengembalikan 160
Sampai baris let dijalankan, value-nya belum ada, meskipun dibaca dari function yang ditulis di atasnya. Bisa dibaca atau tidak ditentukan oleh urutan eksekusi, bukan oleh posisi penulisannya.

Rentang sejak masuk ke scope sampai baris deklarasi let dijalankan disebut TDZ (Temporal Dead Zone — periode ketika sebuah nama tidak bisa dibaca sampai baris deklarasinya dijalankan). Kalau pesan error-nya is not defined, nama itu tidak ada di scope. Kalau before initialization, namanya ada, tapi kamu membacanya sebelum baris deklarasinya dijalankan.

Tampilkan kekurangan belanja untuk gratis ongkos kirim dan poin yang didapat di halaman konfirmasi pesanan. orderTotal sudah dideklarasikan.

① Deklarasikan batas gratis ongkos kirim freeLine dengan let sebagai 5000, lalu di baris bawahnya tampilkan "Kurang N yen lagi untuk gratis ongkos kirim".

② Tulis dulu baris yang menampilkan "Poin yang didapat: N", lalu di baris bawahnya deklarasikan persentase poin pointRate dengan let sebagai 0.05.

③ Periksa berapa baris yang tampil dan error apa yang menghentikan eksekusi.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Didaftarkan Lebih Awal sebagai undefined — Alasan Tidak Memakai var

Di artikel atau library lama, kamu akan menemukan variabel yang dideklarasikan dengan var. Saat kamu menyalin kode yang sudah jalan, var bisa ikut terbawa. Tapi var berbeda dari const dan let dalam dua hal: batas berlakunya variabel, dan apa yang terjadi saat dibaca sebelum deklarasinya.

Deklarasi var juga didaftarkan lebih awal, tapi saat itu juga langsung diisi undefined. Jadi kalau dirujuk di baris di atas deklarasinya, tidak terjadi error — eksekusi lanjut ke baris berikutnya dengan undefined. Batas berlakunya juga bukan kurung kurawal, melainkan seluruh function scope yang membungkusnya.

function checkStock(orderCount) {
  // Dibaca sebelum deklarasi pun tidak berhenti; isinya undefined
  console.log(stock);              // undefined

  var stock = 12;
  console.log(stock);              // 12

  if (orderCount <= stock) {
    var status = "Siap dikirim";           // Dideklarasikan dengan var
    let note = "Dikirim dari gudang A";    // Dideklarasikan dengan let
  }
  // var berlaku di seluruh kurung kurawal function, jadi bisa dibaca di luar if
  console.log(status);             // Siap dikirim
  // console.log(note);            // ReferenceError: note is not defined
}

checkStock(3);
var Tidak Dibatasi Kurung Kurawal
var statusdi-assign di ifKeluar darikurung kurawal ifMasih di dalamfunction: terbaca"Siap dikirim"ditampilkanlet notedi-assign di ifKeluar darikurung kurawal ifnote hanya adadi dalam ifBerhenti denganReferenceError
Meskipun di-assign di dalam if yang sama, value var masih bisa dibaca di baris setelah kurung kurawalnya berakhir. Dengan let, eksekusi berhenti di baris yang membacanya di luar batasnya.

Untuk var, satu nama hanya punya satu variabel di setiap function, jadi mendeklarasikan ulang nama yang sama dengan var di dalam if tidak menghasilkan shadowing — variabel yang sama ditimpa. Tabel di bawah menunjukkan, untuk tiap cara deklarasi, apa yang masuk saat nama didaftarkan sebelum kode berjalan, dan hasilnya kalau dibaca di baris di atas deklarasi.

Cara deklarasiYang masuk saat didaftarkanDibaca di atas deklarasi
var stock = 12Nama dan undefinedLanjut dengan undefined
let stock = 12Hanya nama (tanpa value)Berhenti dengan ReferenceError
const stock = 12Hanya nama (tanpa value)Berhenti dengan ReferenceError

Mengganti var dengan let Membuat Beberapa Baris Berhenti

Kalau kamu mengganti var dengan let di kode lama, baris yang membaca variabel sebelum deklarasinya, atau di luar if, akan berhenti dengan ReferenceError. Baris-baris itu selama ini membaca undefined atau value dari kurung kurawal lain. Untuk memperbaikinya, pindahkan deklarasi ke atas baris yang membacanya, di dalam kurung kurawal yang juga membungkus baris itu.

Lihat bagaimana perilaku variabel var berbeda tergantung posisi deklarasinya. cartTotal sudah dideklarasikan.

① Tulis dulu baris yang menampilkan "Total bayar: N yen", yaitu cartTotal dikurangi discount, lalu di bawahnya deklarasikan discount dengan var sebagai 300.

② Di bawahnya, tampilkan lagi.

③ Kalau totalnya 5000 yen atau lebih, deklarasikan ulang discount dengan var sebagai 800 di dalam if.

④ Di luar if, tampilkan "Diskon: N yen".

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Apa yang terjadi kalau kamu menampilkan unit di baris setelah if (true) { const unit = "yen"; }?

Soal 2Di bagian atas file, kamu menulis function show yang mengembalikan rate. Lalu kamu memanggilnya di baris setelah let rate = 0.1;. Apa yang dikembalikan?

Soal 3Kalau kamu menulis console.log(count); di baris sebelum var count = 3;, apa yang ditampilkan?