Scope, atau serangkaian aturan yang menentukan di mana variabel Anda tinggal, adalah salah satu konsep paling dasar dari bahasa pemrograman apa pun. Ini sangat fundamental, pada kenyataannya, mudah untuk melupakan betapa halus aturannya!
Memahami dengan tepat bagaimana mesin JavaScript "thinks" tentang scope akan membuat Anda tidak bisa menulis bug umum yang dapat menyebabkan masalah, mempersiapkan Anda untuk membungkus head Anda di sekitar penutupan, dan membuat Anda lebih dekat untuk tidak pernah lagi menulis bug.
... Yah, itu akan membantumu mengerti hoisting dan closures, bagaimanapun.
Dalam artikel ini, kita akan melihat:
- dasar-dasar scope di JavaScript
- bagaimana penerjemah memutuskan variabel apa yang termasuk dalam scope apa
- bagaimana hoisting benar-benar bekerja
- bagaimana kata kunci ES6
let
danconst
mengubah permainan
Mari kita selami.
Jika Anda tertarik untuk mempelajari lebih lanjut tentang ES6 dan bagaimana memanfaatkan sintaks dan fitur untuk meningkatkan dan menyederhanakan kode JavaScript Anda, mengapa tidak memeriksa dua program ini:
Lexical Scope
Jika Anda telah menulis mantap baris JavaScript sebelumnya, Anda akan tahu bahwa di mana Anda menjelaskan variabel menentukan di mana Anda dapat menggunakannya. Fakta bahwa visibilitas variabel tergantung pada struktur kode sumber Anda disebut lexical scope.
Ada tiga cara untuk membuat scope dalam JavaScript:
- Membuat fungsi. Variabel yang dideklarasikan di dalam fungsi hanya terlihat di dalam fungsi itu, termasuk dalam fungsi bersarang.
- Deklarasikan variabel dengan
let
atauconst
di dalam blok kode. Deklarasi semacam itu hanya terlihat di dalam blok. - Membuat
catch
blok. Percaya atau tidak, ini sebenarnya menciptakan scope baru!
"use strict"; var mr_global = "Mr Global"; function foo [] { var mrs_local = "Mrs Local"; console.log["I can see " + mr_global + " and " + mrs_local + "."]; function bar [] { console.log["I can also see " + mr_global + " and " + mrs_local + "."]; } } foo[]; // Works as expected try { console.log["But /I/ can't see " + mrs_local + "."]; } catch [err] { console.log["You just got a " + err + "."]; } { let foo = "foo"; const bar = "bar"; console.log["I can use " + foo + bar + " in its block..."]; } try { console.log["But not outside of it."]; } catch [err] { console.log["You just got another " + err + "."]; } // Throws ReferenceError! console.log["Note that " + err + " doesn't exist outside of 'catch'!"]
Potongan di atas menunjukkan ketiga mekanisme scope. Anda dapat menjalankannya di Node atau Firefox, tetapi Chrome tidak bermain bagus dengan let
, belum.
Kami akan membicarakan masing-masing ini dengan sangat detail. Mari kita mulai dengan tampilan mendetail tentang bagaimana JavaScript menggambarkan variabel apa yang termasuk dalam scope apa.
Proses Kompilasi: Tampilan Mata Burung
Ketika Anda menjalankan JavaScript, dua hal terjadi untuk membuatnya bekerja.
- Pertama, sumber Anda dikompilasi.
- Kemudian, kode yang dikompilasi dijalankan.
Selama langkah kompilasi, mesin JavaScript:
- mencatat semua nama variabel Anda
- mendaftarkan mereka dalam scope yang sesuai
- ruang cadangan untuk nilai-nilai mereka
Hanya selama eksekusi bahwa mesin JavaScript benar-benar menetapkan nilai
referensi variabel yang sama dengan nilai tugas mereka. Sampai saat itu, mereka sudah undefined
.
Langkah 1: Compilation
// I can use first_name anywhere in this program var first_name = "Peleke"; function popup [first_name] { // I can only use last_name inside of this function var last_name = "Sengstacke"; alert[first_name + ' ' + last_name]; } popup[first_name];
Mari kita melangkahi apa yang dilakukan oleh kompilator.
Pertama, ia membaca baris var first_name = "Peleke"
. Selanjutnya, ia menentukan scope apa untuk menyimpan variabel ke. Karena kami berada di tingkat teratas skrip, ia menyadari bahwa kami berada di global scope. Kemudian, menyimpan variabel first_name
ke global scope dan menginisialisasi
nilainya ke undefined
.
Kedua, compiler membaca baris dengan function popup [first_name]
. Karena keyword function adalah hal pertama pada baris, itu menciptakan scope baru untuk fungsi, mendaftarkan definisi fungsi ke global scope, dan mengintip ke dalam untuk menemukan deklarasi variabel.
Tentu saja, kompilator menemukan satu. Karena kita memiliki var last_name = "Sengstacke"
di baris pertama dari fungsi kita, kompiler menyimpan variabel last_name
ke lingkup popup
— bukan ke global scope — dan
menetapkan nilainya ke undefined
.
Karena tidak ada lagi deklarasi variabel di dalam fungsi, kompilator kembali ke global scope. Dan karena tidak ada lagi deklarasi variabel di sana, fase ini dilakukan.
Perhatikan bahwa kami belum benar-benar menjalankan apa pun. Pekerjaan kompilator pada titik ini hanya untuk memastikan ia tahu nama semua orang; tidak peduli apa yang mereka lakukan.
Pada titik ini, program kami tahu bahwa:
- Ada variabel
yang disebut
first_name
dalam global scope. - Ada fungsi yang disebut
popup
dalam global scope. - Ada variabel bernama
last_name
di lingkuppopup
. - Nilai-nilai keduanya
first_name
danlast_name
adalahundefined
.
Itu tidak peduli bahwa kami telah diberi nilai-nilai variabel tersebut di tempat lain dalam kode kita. Mesin mengurus itu dalam eksekusi.
Langkah 2: Execution
Selama langkah berikutnya, mesin membaca kode kita lagi, tetapi kali ini, jalankan itu.
Pertama, membaca baris, var first_name = "Peleke"
. Untuk melakukan ini, mesin mencari variabel yang disebut first_name
. Karena compiler telah mendaftarkan variabel dengan nama itu, mesin menemukannya, dan menetapkan nilainya ke "Peleke"
.
Selanjutnya, ia membaca baris, function popup [first_name]
. Karena kita tidak menjalankan fungsi di sini, mesin tidak tertarik dan melewatinya.
Akhirnya, ia membaca baris popup[first_name]
. Karena kita menjalankan fungsi di
sini, mesin:
- mencari nilai
popup
- mencari nilai dari
first_name
- mengeksekusi
popup
sebagai fungsi, meneruskan nilaifirst_name
sebagai parameter
Ketika menjalankan popup
, ia melewati proses yang sama, tetapi kali ini di dalam fungsi popup
. Itu:
- mencari variabel yang bernama
last_name
- set nilai
last_name
sama dengan"Sengstacke"
- terlihat
alert
, mengeksekusinya sebagai fungsi dengan"Peleke Sengstacke"
sebagai parameternya
Ternyata ada lebih banyak yang terjadi di bawah kap mesin daripada yang mungkin kita pikirkan!
Sekarang setelah Anda memahami bagaimana JavaScript membaca dan menjalankan kode yang Anda tulis, kami siap untuk mengatasi sesuatu yang sedikit lebih dekat ke home: cara kerja hoisting.
Hoisting Di Bawah Microscope
Mari kita mulai dengan beberapa kode.
bar[]; function bar [] { if [!foo] { alert[foo + "? This is strange..."]; } var foo = "bar"; } broken[]; // TypeError! var broken = function [] { alert["This alert won't show up!"]; }
Jika Anda menjalankan kode ini, Anda akan memperhatikan tiga hal:
- Anda bisa merujuk ke
foo
sebelum Anda menetapkannya, tetapi nilainyaundefined
. - Anda dapat memanggil
broken
sebelum Anda mendefinisikannya, tetapi Anda akan mendapatkanTypeError
. - Anda dapat memanggil
bar
sebelum menetapkannya, dan berfungsi seperti yang diinginkan.
Hoisting mengacu pada fakta bahwa JavaScript membuat semua nama variabel yang kami nyatakan tersedia di mana saja dalam scope— termasuk sebelum kami menetapkannya.
Tiga kasus dalam cuplikan adalah tiga hal yang perlu Anda ketahui dalam kode Anda sendiri, jadi kami akan melangkah masing-masing satu per satu.
Deklarasi Variabel Hoisting
Ingat, ketika kompiler JavaScript membaca baris seperti var foo = "bar"
, itu:
- mendaftarkan nama
foo
ke scope terdekat - menetapkan nilai
foo
ke undefined
Alasan kita bisa menggunakan foo
sebelum kita menetapkannya karena, ketika mesin mencari variabel
dengan nama itu, itu memang ada. Inilah sebabnya mengapa tidak membuang ReferenceError
.
Sebaliknya, ia mendapat nilai yang undefined
, dan mencoba menggunakannya untuk melakukan apa pun yang Anda minta. Biasanya, ini adalah bug.
Dengan mengingat hal tersebut, kita mungkin membayangkan bahwa apa yang dilihat JavaScript di fungsi bar
kami lebih seperti ini:
function bar [] { var foo; // undefined if [!foo] { // !undefined is true, so alert alert[foo + "? This is strange..."]; } foo = "bar"; }
Ini adalah Aturan Pertama Hoisting, bila Anda akan: Variabel tersedia di seluruh scope
mereka, tetapi memiliki nilai undefined
sampai kode Anda diberikan kepada mereka.
Sebuah idiom JavaScript umum adalah menulis semua deklarasi var
Anda di bagian atas scope mereka, bukan di mana Anda pertama kali menggunakannya. Untuk memfrasekan Doug Crockford, ini membantu kode Anda membaca lebih banyak seperti itu berjalan.
Ketika
Anda memikirkannya, itu masuk akal. Cukup jelas mengapa bar
berperilaku seperti itu ketika kami menulis kode kami dengan cara JavaScript membacanya, bukan? Jadi mengapa tidak menulis seperti itu sepanjang waktu?
Hoisting Function Expressions
Fakta bahwa kami mendapat TypeError
ketika kami mencoba mengeksekusi broken
sebelum kami mendefinisikannya hanyalah kasus khusus dari Aturan Pertama Hoisting.
Kami mendefinisikan sebuah variabel, yang disebut broken
,
dimana register compiler dalam global scope dan set sama ke undefined
. Ketika kami mencoba untuk menjalankannya, mesin mencari nilai yang broken
, menemukan bahwa itu undefined
, dan mencoba mengeksekusi undefined
sebagai fungsi.
Jelas, undefined
bukanlah fungsi — itulah mengapa kita mendapatkan TypeError
!
Deklarasi Fungsi Hoisting
Akhirnya, ingat bahwa kami dapat memanggil bar
sebelum kami mendefinisikannya. Hal ini disebabkan oleh Aturan Kedua
Hoisting: Ketika kompiler JavaScript menemukan deklarasi fungsi, itu membuat kedua nama dan definisinya tersedia di bagian atas scope. Tulis ulang kode kami lagi:
function bar [] { if [!foo] { alert[foo + "? This is strange..."]; } var foo = "bar"; } var broken; // undefined bar[]; // bar is already defined, executes fine broken[]; // Can't execute undefined! broken = function [] { alert["This alert won't show up!"]; }
Sekali lagi, itu lebih masuk akal ketika Anda menulis sebagai JavaScript yang dibaca, bukankah begitu?
Untuk meninjau:
- Nama-nama kedua deklarasi variabel dan ekspresi fungsi tersedia di seluruh scope mereka, tetapi nilainya
undefined
hingga penugasan. - Nama dan definisi deklarasi fungsi tersedia di seluruh scope, bahkan sebelum definisinya.
Sekarang mari kita lihat dua alat baru yang bekerja sedikit berbeda: let
dan const
.
let
, const
, & Zona Mati Temporal
Tidak seperti deklarasi var
, variabel yang dinyatakan dengan let
dan const
tidak mendapatkan hoisted oleh
kompilator.
Setidaknya, tidak persis.
Ingat bagaimana kami dapat memanggil broken
, tetapi mendapat TypeError
karena kami mencoba mengeksekusi undefined
? Jika kami mendefinisikan broken
dengan let
, kami akan mendapatkan ReferenceError
, sebagai gantinya:
"use strict"; // You have to "use strict" to try this in Node broken[]; // ReferenceError! let broken = function [] { alert["This alert won't show up!"]; }
Ketika kompilator JavaScript mendaftarkan variabel ke scope dalam lewatan pertama, ia akan memperlakukan let
dan const
berbeda dari var
.
Ketika menemukan deklarasi var
, kami mendaftarkan nama
variabel ke scope dan segera menginisialisasi nilainya ke undefined
.
Dengan let
, bagaimanapun, kompilator mengerjakan daftar variabel ke scope, tetapi tidak menginisialisasi nilainya ke undefined
. Sebaliknya, ia membiarkan variabel terinisialisasi, sampai mesin menjalankan pernyataan tugas Anda. Mengakses nilai variabel yang belum diinisialisasi akan melemparkan ReferenceError
, yang menjelaskan mengapa cuplikan di atas terlontar ketika kami menjalankannya.
Ruang antara awal bagian atas ruang lingkup pernyataan let
dan pernyataan penugasan disebut Zona Mati Temporal. Nama berasal dari fakta bahwa, meskipun mesin tahu tentang variabel yang disebut foo
di bagian atas scope bar
, variabelnya "mati", karena tidak memiliki nilai.
... Juga karena itu akan membunuh program Anda jika Anda mencoba menggunakannya lebih awal.
Kata kunci const
bekerja dengan cara yang sama seperti let
, dengan dua
perbedaan utama:
- Anda harus menetapkan nilai saat Anda menyatakan dengan
const
. - Anda tidak dapat menetapkan kembali nilai ke variabel yang dideklarasikan dengan
const
.
Ini menjamin bahwa const
akan selalu memiliki nilai yang awalnya Anda tetapkan untuk itu.
// This is legal const React = require['react']; // This is totally not legal const crypto; crypto = require['crypto'];
Block Scope
let
dan const
berbeda dari var
dengan cara lain: ukuran scope mereka.
Ketika Anda mendeklarasikan
variabel dengan var
, itu terlihat sebagai tinggi pada scope chain mungkin - biasanya, di bagian atas deklarasi fungsi terdekat, atau dalam lingkup global, jika Anda menyatakannya di tingkat atas.
Ketika Anda mendeklarasikan variabel dengan let
atau const
, bagaimanapun, terlihat se-lokal mungkin — hanya dalam blok terdekat.
Blok adalah bagian kode yang dilepaskan oleh tanda kurung kurawal, seperti yang Anda lihat dengan blok if
/else
,
for
loop, dan secara eksplisit bagian "blocked" kode, seperti dalam cuplikan ini.
"use strict"; { let foo = "foo"; if [foo] { const bar = "bar"; var foobar = foo + bar; console.log["I can see " + bar + " in this bloc."]; } try { console.log["I can see " + foo + " in this block, but not " + bar + "."]; } catch [err] { console.log["You got a " + err + "."]; } } try { console.log[ foo + bar ]; // Throws because of 'foo', but both are undefined } catch [err] { console.log[ "You just got a " + err + "."]; } console.log[ foobar ]; // Works fine
Jika Anda mendeklarasikan variabel dengan const
atau let
di dalam blok, itu hanya terlihat di dalam blok, dan hanya setelah Anda menetapkannya.
Sebuah variabel yang dinyatakan dengan var
, bagaimanapun, terlihat sejauh mungkin — dalam hal ini, dalam global scope.
Jika Anda tertarik dengan detail detail tentang let
dan const
,
periksa apa yang dikatakan Dr Rauschmayer tentang mereka dalam Exploring ES6: Variable and Scoping, dan lihatlah dokumentasi MDN pada mereka.
Lexical this
& Arrow Functions
Di permukaan, this
sepertinya tidak ada hubungannya dengan scope. Dan, pada kenyataannya, JavaScript
tidak menyelesaikan makna this
sesuai dengan aturan scope yang telah kita bahas di sini.
Setidaknya, biasanya tidak. JavaScript, yang terkenal, tidak menyelesaikan arti kata kunci this
berdasarkan tempat Anda menggunakannya:
var foo = { name: 'Foo', languages: ['Spanish', 'French', 'Italian'], speak : function speak [] { this.languages.forEach[function[language] { console.log[this.name + " speaks " + language + "."]; }] } }; foo.speak[];
Sebagian besar dari kita mengharapkan this
berarti foo
di dalam perulangan forEach
, karena itulah yang dimaksud di luarnya. Dengan kata lain, kami mengharapkan JavaScript untuk menyelesaikan makna
lexically this
.
Tapi itu tidak.
Sebaliknya, ini menciptakan this
baru di dalam setiap fungsi yang Anda definisikan, dan memutuskan apa artinya berdasarkan pada bagaimana Anda memanggil fungsi — bukan di mana Anda mendefinisikannya.
Titik pertama mirip dengan kasus mendefinisikan ulang variabel apa pun dalam child scope:
function foo [] { var bar = "bar"; function baz [] { // Reusing variable names like this is called "shadowing" var bar = "BAR"; console.log[bar]; // BAR } baz[]; } foo[]; // BAR
Ganti bar
dengan this
, dan semuanya harus dibersihkan dengan segera!
Secara
tradisional, menjadikan this
berfungsi seperti yang kita harapkan dari variabel lexically-scoped lama untuk bekerja membutuhkan satu dari dua solusi:
var foo = { name: 'Foo', languages: ['Spanish', 'French', 'Italian'], speak_self : function speak_s [] { var self = this; self.languages.forEach[function[language] { console.log[self.name + " speaks " + language + "."]; }] }, speak_bound : function speak_b [] { this.languages.forEach[function[language] { console.log[this.name + " speaks " + language + "."]; }.bind[foo]]; // More commonly:.bind[this]; } };
Dalam speak_self
, kita menyimpan arti this
ke variabel self
, dan menggunakan variabel itu untuk mendapatkan referensi yang kita inginkan. Di speak_bound
, kami menggunakan bind
untuk secara permanen mengarahkan this
ke objek yang diberikan.
ES2015 membawa kita alternatif baru: fungsi arrow.
Tidak seperti fungsi "normal", fungsi panah tidak membayangi parent scope mereka dengan menetapkan nilai ini
sendiri. Sebaliknya, mereka menyelesaikan maknanya secara lexically.
Dengan kata lain, jika Anda menggunakan this
dalam fungsi arrow, JavaScript mendongak nilainya seperti halnya variabel lain.
Pertama, ia memeriksa scope lokal untuk nilai this
. Karena fungsi arrow tidak mengaturnya, ia tidak akan menemukannya. Selanjutnya, ia memeriksa
parent scope untuk nilai this
. Jika ia menemukannya, ia akan menggunakannya.
Ini memungkinkan kita menulis ulang kode di atas seperti ini:
var foo = { name: 'Foo', languages: ['Spanish', 'French', 'Italian'], speak : function speak [] { this.languages.forEach[[language] => { console.log[this.name + " speaks " + language + "."]; }] } };
Jika Anda ingin detail lebih lanjut tentang fungsi arrow, lihat kursus Envato Tuts+ Instruktur Dan Wellman yang sangat baik di Dasar-Dasar ES6 JavaScript, serta dokumentasi MDN pada fungsi arrow.
Kesimpulan
Kami telah membahas banyak hal sejauh ini! Dalam artikel ini, Anda telah belajar bahwa:
- Variabel didaftarkan ke scope mereka selama kompilasi, dan terkait dengan nilai tugas mereka selama eksekusi.
- Mengacu pada variabel yang dideklarasikan dengan
let
atauconst
sebelum tugas melemparReferenceError
, dan variabel tersebut dicakup ke blok terdekat. - Fungsi panah memungkinkan kita untuk mencapai pengikatan leksikal
this
, dan memotong binding dinamis tradisional.
Anda juga telah melihat dua aturan hoisting:
- Aturan Pertama Hoisting: Itu ekspresi fungsi dan deklarasi
var
tersedia di seluruh scope di mana mereka didefinisikan, tetapi memiliki nilaiundefined
sampai pernyataan tugas Anda eksekusi. - Aturan Kedua Hoisting: Bahwa nama-nama deklarasi fungsi dan tubuh mereka tersedia di seluruh scope di mana mereka didefinisikan.
Langkah selanjutnya yang baik adalah menggunakan pengetahuan baru Anda tentang scope JavaScript untuk membungkus head Anda di sekitar penutupan. Untuk itu, periksa Scopes & Closures Kyle Simpson.
Akhirnya, ada
banyak lagi yang bisa dikatakan tentang this
daripada yang bisa saya bahas di sini. Jika kata kunci masih tampak seperti banyak sihir hitam, lihat this & Object Prototypes untuk mendapatkan head Anda di sekitarnya.
Sementara itu, ambillah apa yang telah Anda pelajari dan tulis lebih sedikit bug!