Kebanyakan orang memulakan JavaScript dengan membina sesuatu. Anda menyambungkan butang, mengambil beberapa data, dan melihat DOM berubah. Kemudian, abstraksi mula bocor. Pepijat muncul tanpa sebab yang munasabah: pemboleh ubah wujud sebelum pengisytiharannya, fungsi mengingati pemboleh ubah yang sepatutnya tidak boleh dicapainya, dan kata kunci this merujuk kepada window, sebuah butang, atau tiada apa-apa langsung. Itulah selalunya saat anda menyedari bahawa anda perlu melihat apa yang berlaku di sebalik tabir dan memahami apa yang sebenarnya dilakukan oleh enjin tersebut.

Konteks Pelaksanaan: Persediaan Dua Fasa

Apabila enjin JavaScript dalam pelayar atau Node.js anda menjumpai satu skrip, ia tidak sekadar membaca fail dari atas ke bawah seperti seseorang yang sedang membaca halaman. Sebaliknya, ia membina konteks pelaksanaan, sebuah bekas yang memegang segala yang diperlukan untuk menjalankan satu bahagian kod tertentu. Setiap konteks pelaksanaan melalui dua fasa yang berbeza.

Fasa Penciptaan Memori. Semasa laluan awal ini, enjin akan mengimbas keseluruhan skop dan memperuntukkan memori untuk setiap pengisytiharan pemboleh ubah dan fungsi yang ditemuinya. Jika ia melihat var, ia akan menyediakan ruang dan menyimpan undefined sebagai penanda tempat. Jika ia melihat pengisytiharan fungsi, ia akan menyimpan keseluruhan badan fungsi tersebut. Inilah sebabnya mengapa fungsi yang diisytiharkan dengan kata kunci function tradisional boleh dipanggil dari baris yang kelihatan lebih awal dalam skop yang sama. Enjin sudah mengetahuinya sebelum ia mula melaksanakan kod tersebut.

Fasa Pelaksanaan Kod. Sekarang enjin menjalankan kod anda baris demi baris. Penugasan berlaku di sini. Ekspresi dinilai. Fungsi dipanggil. Jika anda menulis var name = "Alice";, penanda tempat daripada fasa pertama akhirnya digantikan dengan rentetan (string) tersebut. Memahami tingkah laku dua-laluan ini akan menjelaskan banyak kekeliruan. Enjin tersebut tidak cuai; ia sedang mengikuti rutin persediaan yang ketat.

Pemboleh Ubah dan Zon Mati Temporal

Memilih antara var, let, dan const bukan sekadar pilihan gaya. var adalah berasaskan skop fungsi, yang bermaksud ia mengabaikan kurungan {} sepenuhnya. Isytiharkan ia di dalam blok if, dan ia akan bocor ke luar. Tingkah laku itu mungkin masuk akal dalam JavaScript awal, tetapi dalam aplikasi moden, ia menyebabkan masalah penyelenggaraan yang nyata. let dan const pula berasaskan skop blok. Ia menghormati kurungan dan hilang apabila blok tersebut tamat.

Terdapat juga perbezaan halus tetapi penting dalam cara hoisting melayan mereka. Pengisytiharan var diangkat (hoisted) dan segera diinisialisasi dengan undefined. let dan const secara teknikalnya juga diangkat; enjin tahu ia wujud sebelum ia sampai ke baris pengisytiharan. Tetapi ia tidak diinisialisasi. Ia berada dalam keadaan tergantung yang dipanggil zon mati temporal (temporal dead zone). Jika anda cuba membacanya sebelum baris pengisytiharan dilaksanakan, anda akan mendapat ReferenceError yang nyata dan bukannya undefined yang tersembunyi. Kegagalan (crash) tersebut sebenarnya sangat membantu. Ia menghalang logik daripada diteruskan berdasarkan data yang tidak diinisialisasi.

Skop Leksikal dan Closure

Skop menjawab soalan mudah: di manakah saya boleh mengakses pemboleh ubah ini? JavaScript menggunakan skop leksikal, yang bermaksud hak capaian sesuatu fungsi ditentukan oleh di mana ia ditulis secara fizikal dalam kod sumber, bukan oleh di mana ia akhirnya dipanggil. Jika anda mendefinisikan satu fungsi di dalam fungsi yang lain, fungsi dalaman boleh mencapai ke luar untuk membaca pemboleh ubah daripada fungsi induknya. Fungsi luaran pula tidak boleh mencapai ke dalam. Hubungan ini adalah statik. Anda boleh menghantar fungsi dalaman itu merentasi modul, menyimpannya dalam pemboleh ubah global, dan memanggilnya dari fail yang berbeza sama sekali. Ia tetap mengingati pemboleh ubah daripada skop tempat ia dilahirkan.

Tingkah laku itu secara semula jadi menghasilkan closure. Closure terbentuk apabila fungsi dalaman mengekalkan rujukan kepada pemboleh ubah daripada skop luarnya. Walaupun selepas fungsi luaran selesai dilaksanakan dan pemboleh ubah tempatannya sepatutnya telah dibersihkan melalui pengumpulan sampah (garbage collected), JavaScript mengekalkannya dalam memori kerana fungsi dalaman masih memerlukannya. Fungsi dalaman membawa persekitaran sekelilingnya bersamanya.

Ini bukan sekadar perincian akademik. Closure memberikan anda cara praktikal untuk mencipta keadaan peribadi (private state) dalam bahasa yang kekurangan pengubah suai capaian (access modifiers) yang eksplisit.

function makeCounter() {
  let count = 0;
  return function() {
    count = count + 1;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

Di sini, count disembunyikan. Tiada apa-apa di luar fungsi yang dikembalikan dapat menetapkan semula atau membacanya secara langsung. Itulah pemboleh ubah peribadi yang dibina hanya daripada mekanik skop sahaja.

Hoisting dalam Praktis

It is common to hear that JavaScript "moves declarations to the top." That is a useful mental model, but the code does not actually get rewritten. During the memory creation phase, the engine simply registers declarations before execution begins. A statement like var x = 5; behaves as if the declaration and initialization are decoupled. The declaration var x; is processed early and initialized to undefined. The assignment x = 5; stays exactly where you wrote it and runs during the execution phase.

Because of this, var can lead to surprising results. A variable used near the top of a function might hold undefined even though a giant assignment sits at the bottom. Using let and const eliminates that particular foot-gun because the temporal dead zone forces you to keep your declarations above your usage.

How this Gets Its Value

If execution context and scope determine where variables live, this determines which object is currently in charge. Unlike lexical variables, this is not decided by where a function is written. It is decided entirely by how the function is called.

Default binding occurs when you invoke a plain, standalone function. In non-strict mode, this falls back to the global object. In a browser, that is window. Call a function without any context, and you might accidentally be touching global state without realizing it.

Implicit binding happens when you call a function as a method on an object. If you write user.sayName(), the dot quietly tells the engine to set this to user for the duration of that call. The site of the call matters more than the site of the definition.

Explicit binding lets you override everything manually. call() and apply() invoke a function immediately while forcing this to be a specific object you provide. The only difference between them is how arguments are passed: call takes a comma-separated list, while apply takes an array. bind() works differently. It does not invoke the function right away. Instead, it returns a new function with this permanently locked to the value you supplied. This is invaluable for callbacks that might otherwise lose their context when passed around.

New binding comes into play when you use the new keyword in front of a function call. The engine constructs a brand-new empty object, sets its prototype linkage, and points this inside the constructor to that fresh instance.

Writing Code That Lasts

Understanding mechanics is only half the craft. The other half is writing code that humans can read six months from now.

DRY (Don't Repeat Yourself) sounds obvious, but it is violated constantly. If you find yourself writing the same validation logic or API call pattern in three different files, extract it. Write one function. One source of truth means one place to update when requirements change, and that saves time in ways that are difficult to overstate.

KISS (Keep It Simple, Stupid) is a defense against ego. Nested ternaries and one-liner closures feel clever, but they cost hours during debugging. The closure pattern I showed earlier is powerful, yet nesting five levels deep just because you can is a mistake. Simple code survives team turnover, production incidents, and those middle-of-the-night alerts when something breaks and nobody remembers why.

The Real Payoff

Studying execution