Mari kita mulai dengan pertanyaan sederhana:
<script type="text/javascript">
peringatan(saya); // ?
var saya = 1;
</skrip>
Hasil keluaran tidak terdefinisi. Fenomena ini disebut "pra-parsing": mesin JavaScript akan mengurai variabel var dan definisi fungsi terlebih dahulu. Kode tidak akan dieksekusi sampai pra-parsing selesai. Jika aliran dokumen berisi beberapa segmen kode skrip (kode js dipisahkan dengan tag skrip atau file js yang diimpor), urutan yang berjalan adalah:
langkah1. Baca segmen kode pertama
langkah 2. Lakukan analisis sintaksis. Jika ada kesalahan, kesalahan sintaksis akan dilaporkan (seperti tanda kurung tidak cocok, dll.) dan lompat ke langkah5.
langkah 3. Lakukan "pra-parsing" variabel var dan definisi fungsi (tidak ada kesalahan yang akan dilaporkan, karena hanya deklarasi yang benar yang diurai)
langkah4. Jalankan segmen kode dan laporkan kesalahan jika ada kesalahan (misalnya, variabel tidak terdefinisi)
langkah5. Jika ada segmen kode lain, baca segmen kode berikutnya dan ulangi langkah2.
langkah 6. Pada akhir analisa diatas sudah mampu menjelaskan banyak permasalahan, namun saya selalu merasa ada yang kurang. Misalnya, pada langkah 3, apa sebenarnya "pra-parsing" itu? Dan pada langkah ke 4, lihat contoh berikut:
<script type="text/javascript">
peringatan(i); // kesalahan: i tidak ditentukan.
saya = 1;
</skrip>
Mengapa kalimat pertama menyebabkan kesalahan? Dalam JavaScript, bukankah variabel harus tidak terdefinisi?
Waktu proses kompilasi berlalu seperti kuda putih, dan saya membuka "Prinsip Kompilasi" di sebelah rak buku seolah-olah berada di dunia yang jauh. Ada catatan ini di ruang kosong yang familier namun asing:
Untuk bahasa kompilasi tradisional , langkah kompilasi dibagi menjadi: analisis leksikal dan analisis sintaksis, pemeriksaan semantik, optimasi kode, dan pembuatan byte.
Namun untuk bahasa yang ditafsirkan, setelah pohon sintaksis diperoleh melalui analisis leksikal dan analisis sintaksis, interpretasi dan eksekusi dapat dimulai.
Sederhananya, analisis leksikal adalah mengubah aliran karakter (char stream) menjadi aliran token (token stream), seperti mengubah c = a - b menjadi:
NAME "c";
SAMA
NAMA "a"
MINUS
NAMA "b"
TITIK KOMA
Di atas hanyalah contoh. Untuk informasi lebih lanjut, silakan lihat Analisis Leksikal.
Bab 2 dari "Panduan Definitif JavaScript" membahas tentang Struktur Leksikal, yang juga dijelaskan dalam ECMA-262. Struktur leksikal merupakan dasar suatu bahasa dan mudah dikuasai. Adapun penerapan analisis leksikal merupakan bidang penelitian lain dan tidak akan dieksplorasi di sini.
Kita dapat menggunakan analogi bahasa alami. Analisis leksikal adalah terjemahan sulit satu-ke-satu. Misalnya, jika sebuah paragraf bahasa Inggris diterjemahkan ke dalam bahasa Mandarin kata demi kata, yang kita dapatkan adalah sekumpulan aliran token, yang mana sulit. untuk memahami. Terjemahan lebih lanjut memerlukan analisis tata bahasa. Gambar berikut adalah pohon sintaksis dari pernyataan bersyarat:
Saat membuat pohon sintaksis, jika ternyata tidak dapat dibuat, seperti if(a { i = 2; }, kesalahan sintaksis akan dilaporkan dan penguraian seluruh blok kode akan berakhir. Ini adalah langkah 2 di awal artikel ini.
Melalui analisis sintaksis, konstruksi Setelah pohon sintaksis, kalimat yang diterjemahkan mungkin masih ambigu, dan pemeriksaan semantik lebih lanjut diperlukan untuk bahasa tradisional yang diketik dengan kuat, bagian utama dari pemeriksaan semantik adalah pemeriksaan tipe, seperti parameter fungsi sebenarnya dan apakah tipe parameter formal cocok. Untuk bahasa yang diketik dengan lemah, langkah ini mungkin tidak tersedia (saya memiliki energi terbatas dan tidak punya waktu untuk melihat implementasi mesin JS, jadi saya tidak yakin apakah ada adalah ada. langkah pemeriksaan semantik di mesin JS)
. Ternyata untuk mesin JavaScript harus ada analisis leksikal dan analisis sintaksis, kemudian mungkin ada langkah-langkah seperti pemeriksaan semantik dan optimasi kode proses kompilasi, tetapi bahasa yang ditafsirkan tidak dikompilasi menjadi kode biner), kode akan mulai dieksekusi.
Proses kompilasi di atas masih belum bisa menjelaskan "pra-parsing" di awal artikel proses kode JavaScript.
Zhou Aimin berkata dalam "Esensi Bahasa JavaScript". Bagian kedua dari "Praktik Pemrograman" memiliki analisis yang sangat cermat mengenai hal ini. Berikut adalah beberapa wawasan saya:
Melalui kompilasi, kode JavaScript telah diterjemahkan ke dalam pohon sintaksis, dan kemudian akan segera dieksekusi sesuai dengan pohon sintaksis,
yang memerlukan eksekusi lebih lanjut. Memahami mekanisme cakupan JavaScript. JavaScript menggunakan cakupan leksikal. Dalam istilah awam, cakupan variabel JavaScript ditentukan ketika variabel tersebut didefinisikan daripada kapan dieksekusi. Artinya, cakupan leksikal bergantung pada kode sumber. Kompilator dapat menentukannya melalui analisis statis, sehingga cakupan leksikal juga disebut cakupan statis dan eval tidak dapat diwujudkan hanya melalui teknologi statis. Faktanya, kita hanya dapat berbicara tentang mekanisme cakupan JS. Sangat dekat dengan cakupan leksikal.
Ketika mesin JS menjalankan setiap instance fungsi, ini menciptakan konteks eksekusi objek panggilan. Objek panggilan adalah struktur scriptObject yang digunakan untuk menyimpan tabel variabel internal. Struktur analisis sintaksis seperti varDecls, tabel fungsi tertanam funDecls, dan nilai atas daftar referensi induk (catatan: informasi seperti varDecls dan funDecls diperoleh selama proses. tahap analisis sintaksis dan disimpan dalam pohon sintaksis. Ketika instance fungsi dijalankan, informasi ini akan Disalin dari pohon sintaksis ke scriptObject). .
Lingkup leksikal adalah mekanisme cakupan JS, dan Anda juga perlu memahami metode implementasinya. Rantai cakupan adalah mekanisme pencarian nama. Ini pertama kali mencari scriptObject di lingkungan eksekusi saat ini. Jika tidak ditemukan, ia akan mengikuti upvalue ke scriptObject induk dan mencari objek global.
Ketika sebuah instance fungsi dijalankan, penutupan dibuat atau dikaitkan dengannya. scriptObject digunakan untuk menyimpan tabel variabel yang terkait dengan fungsi secara statis, sedangkan penutupan secara dinamis menyimpan tabel variabel ini dan nilai berjalannya selama eksekusi. Siklus hidup penutupan mungkin lebih lama dibandingkan siklus hidup instance fungsi. Contoh fungsi akan secara otomatis dimusnahkan setelah referensi aktif menjadi kosong, dan penutupan akan didaur ulang oleh mesin JS setelah referensi data menjadi kosong (dalam beberapa kasus, tidak akan didaur ulang secara otomatis, sehingga mengakibatkan kebocoran memori).
Jangan terintimidasi oleh kumpulan kata benda di atas. Setelah Anda memahami konsep lingkungan eksekusi, objek pemanggilan, penutupan, cakupan leksikal, dan rantai cakupan, banyak fenomena dalam bahasa JS yang dapat diselesaikan dengan mudah.
Ringkasan Pada titik ini, pertanyaan-pertanyaan di awal artikel dapat dijelaskan dengan sangat jelas:
Apa yang disebut "pra-parsing" pada langkah 3 sebenarnya diselesaikan pada tahap analisis sintaksis pada langkah 2 dan disimpan dalam pohon sintaksis. Ketika sebuah instance fungsi dijalankan, varDelcs dan funcDecls akan disalin dari pohon sintaksis ke scriptObject lingkungan eksekusi.
Pada langkah 4, variabel yang tidak terdefinisi berarti bahwa variabel tersebut tidak dapat ditemukan di tabel variabel scriptObject. Mesin JS akan mencari ke atas sepanjang nilai upvalue scriptObject. Jika tidak ada yang ditemukan, operasi tulis i = 1; i = 1; menambahkan atribut baru ke objek jendela. Untuk operasi baca, jika scriptObject yang ditelusuri kembali ke lingkungan eksekusi global tidak dapat ditemukan, kesalahan runtime akan terjadi.
Setelah dipahami, kabut menghilang dan bunga-bunga bermekaran, dan langit menjadi cerah.
Terakhir, saya meninggalkan Anda dengan sebuah pertanyaan:
<script type="text/javascript">
var arg = 1;
fungsi foo(arg) {
peringatan(argumen);
var arg = 2;
}
foo(3);
</skrip>
Apa keluaran dari peringatan?