Cakupan dan konteks dalam JavaScript bersifat unik untuk bahasa tersebut, sebagian berkat fleksibilitas yang dibawanya. Setiap fungsi memiliki konteks dan cakupan variabel yang berbeda. Konsep-konsep ini mendasari beberapa pola desain yang kuat dalam JavaScript. Namun, hal ini juga menimbulkan kebingungan besar bagi pengembang. Berikut ini secara komprehensif mengungkapkan perbedaan antara konteks dan cakupan dalam JavaScript, dan bagaimana berbagai pola desain menggunakannya.
konteks vs ruang lingkup
Hal pertama yang perlu diperjelas adalah bahwa konteks dan ruang lingkup merupakan konsep yang berbeda. Selama bertahun-tahun saya memperhatikan bahwa banyak pengembang sering mengacaukan kedua istilah ini, salah menggambarkan satu sama lain. Agar adil, istilah-istilah ini menjadi sangat membingungkan.
Setiap pemanggilan fungsi memiliki cakupan dan konteks yang terkait dengannya. Pada dasarnya, ruang lingkup berbasis fungsi dan konteks berbasis objek. Dengan kata lain, cakupan terkait dengan akses variabel pada setiap pemanggilan fungsi, dan setiap pemanggilan bersifat independen. Konteksnya selalu berupa nilai kata kunci this, yang merupakan referensi ke objek yang memanggil kode yang dapat dieksekusi saat ini.
ruang lingkup variabel
Variabel dapat didefinisikan dalam cakupan lokal atau global, yang menghasilkan akses variabel runtime dari cakupan berbeda. Variabel global perlu dideklarasikan di luar isi fungsi, ada selama proses berjalan, dan dapat diakses dan dimodifikasi dalam lingkup apa pun. Variabel lokal hanya didefinisikan di dalam badan fungsi dan memiliki cakupan berbeda untuk setiap pemanggilan fungsi. Topik ini adalah penugasan, evaluasi, dan pengoperasian nilai hanya dalam panggilan, dan nilai di luar cakupan tidak dapat diakses.
Saat ini, JavaScript tidak mendukung cakupan tingkat blok. Cakupan tingkat blok mengacu pada variabel yang ditentukan dalam blok pernyataan seperti pernyataan if, pernyataan switch, pernyataan loop, dll. Ini berarti bahwa variabel tidak dapat diakses di luar blok pernyataan. Saat ini variabel apa pun yang didefinisikan dalam blok pernyataan dapat diakses di luar blok pernyataan. Namun, hal ini akan segera berubah, karena kata kunci let telah resmi ditambahkan ke spesifikasi ES6. Gunakan ini sebagai pengganti kata kunci var untuk mendeklarasikan variabel lokal sebagai cakupan tingkat blok.
konteks "ini".
Konteksnya biasanya bergantung pada bagaimana suatu fungsi dipanggil. Ketika suatu fungsi dipanggil sebagai metode pada suatu objek, ini diatur ke objek tempat metode tersebut dipanggil:
Copy kode kodenya sebagai berikut:
var objek = {
foo: fungsi(){
waspada(ini === objek);
}
};
objek.foo(); // benar
Prinsip yang sama berlaku ketika memanggil fungsi untuk membuat instance objek menggunakan operator baru. Ketika dipanggil dengan cara ini, nilai this akan ditetapkan ke instance yang baru dibuat:
Copy kode kodenya sebagai berikut:
fungsi foo(){
waspada(ini);
}
foo() // jendela
foo baru() // foo
Saat memanggil fungsi tidak terikat, ini akan disetel ke konteks global atau objek jendela (jika di browser) secara default. Namun, jika fungsi dijalankan dalam mode ketat ("gunakan ketat"), nilainya akan disetel ke tidak ditentukan secara default.
Konteks eksekusi dan rantai cakupan
JavaScript adalah bahasa single-thread, yang artinya hanya dapat melakukan satu hal dalam satu waktu di browser. Saat penerjemah JavaScript pertama kali mengeksekusi kode, kode tersebut terlebih dahulu ditetapkan secara default ke konteks global. Setiap panggilan ke suatu fungsi menciptakan konteks eksekusi baru.
Kebingungan sering terjadi di sini. Istilah “konteks eksekusi” di sini berarti ruang lingkup, bukan konteks seperti yang dibahas di atas. Ini adalah penamaan yang buruk, namun istilah ini ditentukan oleh spesifikasi ECMAScript dan tidak ada pilihan selain mematuhinya.
Setiap kali konteks eksekusi baru dibuat, konteks tersebut ditambahkan ke bagian atas rantai cakupan dan menjadi tumpukan eksekusi atau panggilan. Browser selalu berjalan dalam konteks eksekusi saat ini di bagian atas rantai cakupan. Setelah selesai, itu (konteks eksekusi saat ini) dihapus dari atas tumpukan dan kontrol dikembalikan ke konteks eksekusi sebelumnya. Misalnya:
Copy kode kodenya sebagai berikut:
fungsi dulu(){
Kedua();
fungsi kedua(){
ketiga();
fungsi ketiga(){
keempat();
fungsi keempat(){
//melakukan sesuatu
}
}
}
}
Pertama();
Menjalankan kode sebelumnya akan menyebabkan fungsi bersarang dijalankan dari atas ke bawah hingga fungsi keempat. Saat ini, rantai cakupan dari atas ke bawah adalah: keempat, ketiga, kedua, pertama, global. Fungsi keempat dapat mengakses variabel global dan variabel apa pun yang ditentukan dalam fungsi pertama, kedua, dan ketiga seperti halnya variabelnya sendiri. Setelah fungsi keempat menyelesaikan eksekusi, konteks keempat akan dihapus dari bagian atas rantai cakupan dan eksekusi akan kembali ke fungsi ketiga. Proses ini berlanjut hingga semua kode selesai dieksekusi.
Konflik penamaan variabel antara konteks eksekusi yang berbeda diselesaikan dengan meningkatkan rantai cakupan, dari lokal ke global. Artinya variabel lokal dengan nama yang sama memiliki prioritas lebih tinggi dalam rantai cakupan.
Sederhananya, setiap kali Anda mencoba mengakses suatu variabel dalam konteks eksekusi fungsi, proses pencarian selalu dimulai dari objek variabel itu sendiri. Jika variabel yang Anda cari tidak ditemukan di objek variabel Anda sendiri, lanjutkan pencarian rantai cakupannya. Ini akan menaiki rantai cakupan dan memeriksa setiap objek variabel konteks eksekusi untuk menemukan nilai yang cocok dengan nama variabel.
penutup
Penutupan terbentuk ketika fungsi bersarang diakses di luar definisinya (cakupan) sehingga dapat dieksekusi setelah fungsi luarnya kembali. Itu (penutupan) mempertahankan akses (dalam fungsi dalam) ke variabel lokal, argumen dan deklarasi fungsi di fungsi luar. Enkapsulasi memungkinkan kita menyembunyikan dan melindungi konteks eksekusi dari lingkup luar, sekaligus mengekspos antarmuka publik yang dapat digunakan untuk melakukan operasi lebih lanjut. Contoh sederhananya terlihat seperti ini:
Copy kode kodenya sebagai berikut:
fungsi foo(){
var local = 'variabel pribadi';
kembalikan bilah fungsi(){
kembalikan lokal;
}
}
var getLocalVariable = foo();
getLocalVariable() // variabel pribadi
Salah satu jenis penutupan yang paling populer adalah pola modul yang terkenal. Ini memungkinkan Anda untuk mengejek anggota publik, swasta, dan istimewa:
Copy kode kodenya sebagai berikut:
var Modul = (fungsi(){
var privateProperty = 'foo';
fungsi Metode Pribadi(args){
//melakukan sesuatu
}
kembali {
Properti publik: "",
Metode publik: fungsi(args){
//melakukan sesuatu
},
Metode istimewa: fungsi(args){
metode pribadi(args);
}
}
})();
Modul sebenarnya agak mirip dengan singleton, menambahkan sepasang tanda kurung di akhir dan mengeksekusinya segera setelah interpreter selesai menafsirkannya (segera jalankan fungsinya). Satu-satunya anggota eksternal yang tersedia dari konteks eksekusi penutupan adalah metode publik dan properti di objek yang dikembalikan (seperti Module.publicMethod). Namun, semua properti dan metode privat akan ada sepanjang siklus hidup program, karena konteks eksekusi dilindungi (penutupan), dan interaksi dengan variabel dilakukan melalui metode publik.
Jenis penutupan lainnya disebut ekspresi fungsi yang segera dipanggil IIFE, yang tidak lebih dari fungsi anonim yang dipanggil sendiri dalam konteks jendela.
Copy kode kodenya sebagai berikut:
fungsi(jendela){
var a = 'foo', b = 'bar';
fungsi pribadi(){
//melakukan sesuatu
}
jendela.Modul = {
publik: fungsi(){
//melakukan sesuatu
}
};
})(ini);
Ekspresi ini sangat berguna untuk melindungi namespace global. Semua variabel yang dideklarasikan dalam badan fungsi adalah variabel lokal dan bertahan di seluruh lingkungan runtime melalui penutupan. Cara merangkum kode sumber ini sangat populer baik untuk program maupun kerangka kerja, biasanya memperlihatkan satu antarmuka global untuk berinteraksi dengan dunia luar.
Hubungi dan Lamar
Kedua metode sederhana ini, yang dibangun di semua fungsi, memungkinkan fungsi dijalankan dalam konteks khusus. Fungsi panggilan memerlukan daftar parameter sedangkan fungsi apply memungkinkan Anda meneruskan parameter sebagai array:
Copy kode kodenya sebagai berikut:
fungsi pengguna(pertama, terakhir, umur){
//melakukan sesuatu
}
user.call(jendela, 'John', 'Doe', 30);
user.apply(jendela, ['John', 'Doe', 30]);
Hasil eksekusinya sama, fungsi pengguna dipanggil pada konteks jendela dan disediakan tiga parameter yang sama.
ECMAScript 5 (ES5) memperkenalkan metode Function.prototype.bind untuk mengontrol konteks, yang mengembalikan fungsi baru yang terikat secara permanen ke argumen pertama metode bind, terlepas dari cara pemanggilan fungsi tersebut. Ini memperbaiki konteks fungsi melalui penutupan. Berikut ini solusi untuk browser yang tidak mendukungnya:
Copy kode kodenya sebagai berikut:
if(!('bind' di Function.prototype)){
Fungsi.prototipe.mengikat = fungsi(){
var fn = ini, konteks = argumen[0], args = Array.prototype.slice.call(argumen, 1);
mengembalikan fungsi(){
return fn.apply(konteks, args);
}
}
}
Ini biasanya digunakan dalam kehilangan konteks: berorientasi objek dan pemrosesan acara. Hal ini diperlukan karena metode addEventListener pada node selalu mempertahankan konteks eksekusi fungsi sebagai node yang terikat dengan event handler, dan ini penting. Namun, jika Anda menggunakan teknik berorientasi objek tingkat lanjut dan perlu mempertahankan konteks fungsi panggilan balik sebagai contoh metode, Anda harus menyesuaikan konteks secara manual. Inilah kemudahan yang dibawa oleh bind:
Copy kode kodenya sebagai berikut:
fungsi Kelas Saya(){
this.element = dokumen.createElement('div');
this.element.addEventListener('klik', this.onClick.bind(ini), false);
}
Kelas Saya.prototipe.onClick = function(e){
//melakukan sesuatu
};
Saat melihat kembali kode sumber fungsi pengikatan, Anda mungkin memperhatikan baris kode yang relatif sederhana berikut ini, yang memanggil metode pada Array:
Copy kode kodenya sebagai berikut:
Array.prototype.slice.call(argumen, 1);
Menariknya, penting untuk dicatat di sini bahwa objek argumentasi sebenarnya bukan sebuah array, namun sering digambarkan sebagai objek mirip array, seperti nodelist (hasil yang dikembalikan oleh metode document.getElementsByTagName()). Mereka berisi atribut panjang, dan nilainya dapat diindeks, tetapi mereka masih bukan array karena tidak mendukung metode array asli seperti irisan dan push. Namun, karena perilakunya mirip dengan array, metode array dapat dipanggil dan dibajak. Jika Anda ingin mengeksekusi metode array dalam konteks seperti array, ikuti contoh di atas.
Teknik pemanggilan metode objek lain ini juga diterapkan pada berorientasi objek, ketika meniru pewarisan klasik (pewarisan kelas) dalam JavaScript:
Copy kode kodenya sebagai berikut:
Kelas Saya.prototipe.init = fungsi(){
// memanggil metode init superclass dalam konteks instance "MyClass".
MySuperClass.prototype.init.apply(ini, argumen);
}
Kita dapat mereproduksi pola desain yang kuat ini dengan memanggil metode superkelas (MySuperClass) dalam instance subkelas (MyClass).
sebagai kesimpulan
Sangat penting untuk memahami konsep-konsep ini sebelum Anda mulai mempelajari pola desain tingkat lanjut, karena cakupan dan konteks memainkan peran penting dan mendasar dalam JavaScript modern. Apakah kita berbicara tentang penutupan, berorientasi objek, dan pewarisan atau berbagai implementasi asli, konteks dan ruang lingkup memainkan peran penting. Jika tujuan Anda adalah menguasai bahasa JavaScript dan mendapatkan pemahaman mendalam tentang komponen-komponennya, cakupan dan konteks harus menjadi titik awal Anda.
Suplemen penerjemah
Fungsi pengikatan yang diterapkan oleh penulis tidak lengkap. Parameter tidak dapat diteruskan saat memanggil fungsi yang dikembalikan oleh pengikatan.
Copy kode kodenya sebagai berikut:
if(!('bind' di Function.prototype)){
Fungsi.prototipe.mengikat = fungsi(){
var fn = ini, konteks = argumen[0], args = Array.prototype.slice.call(argumen, 1);
mengembalikan fungsi(){
kembali fn.apply(konteks, args.concat(argumen));//diperbaiki
}
}
}