Cara menggunakan javascript scope explained

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 dan const 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:

  1. Membuat fungsi. Variabel yang dideklarasikan di dalam fungsi hanya terlihat di dalam fungsi itu, termasuk dalam fungsi bersarang.
  2. Deklarasikan variabel dengan let atau const di dalam blok kode. Deklarasi semacam itu hanya terlihat di dalam blok.
  3. 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.

  1. Pertama, sumber Anda dikompilasi.
  2. Kemudian, kode yang dikompilasi dijalankan.

Selama langkah kompilasi, mesin JavaScript:

  1. mencatat semua nama variabel Anda
  2. mendaftarkan mereka dalam scope yang sesuai
  3. 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:

  1. Ada variabel yang disebut first_name dalam global scope.
  2. Ada fungsi yang disebut popup dalam global scope.
  3. Ada variabel bernama last_name di lingkup popup.
  4. Nilai-nilai keduanya first_name dan last_name adalah undefined.

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:

  1. mencari nilai popup
  2. mencari nilai dari first_name
  3. mengeksekusi popup sebagai fungsi, meneruskan nilai first_name sebagai parameter

Ketika menjalankan popup, ia melewati proses yang sama, tetapi kali ini di dalam fungsi popup. Itu:

  1. mencari variabel yang bernama last_name
  2. set nilai last_name sama dengan "Sengstacke"
  3. 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:

  1. Anda bisa merujuk ke foo sebelum Anda menetapkannya, tetapi nilainya undefined.
  2. Anda dapat memanggil broken sebelum Anda mendefinisikannya, tetapi Anda akan mendapatkan TypeError.
  3. 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:

  1. mendaftarkan nama foo ke scope terdekat
  2. 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:

  1. Nama-nama kedua deklarasi variabel dan ekspresi fungsi tersedia di seluruh scope mereka, tetapi nilainya undefined hingga penugasan.
  2. 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:

  1. Anda harus menetapkan nilai saat Anda menyatakan dengan const.
  2. 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 atau const sebelum tugas melempar ReferenceError, 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 nilai undefined 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!

Apa itu scope di JavaScript?

Salah satu variable JavaScript adalah Scope. Variable Scope merupakan wilayah program Anda yang sudah ditentukan. Variabel JavaScript hanya memiliki dua scope. Terdapat dua jenis variabel scope yang ada di JavaScript yaitu Global Variable dan Local Variable.

Apa itu scope dalam program?

Variabel Scope (atau ruang lingkup variabel) adalah jangkauan kode program dimana perintah program masih bisa mengakses sebuah variabel. Jika kita mendefenisikan sebuah variabel pada satu file PHP, maka variabel tersebut dapat diakses oleh seluruh kode program pada halaman yang sama.

Apa itu Lexical Scope?

Lexical scope adalah scope yang dibaca saat kode JavaScript melalui proses compile, atau sering disebut compile-time. Lexical scope ini yang mengatur di scope mana kita harus mencari sebuah variable.