15 Desember 2021

Lingkup variabel, closure

Javascript adalah bahasa yang berorientasi-fungsi. Itu memberikan kita banyak kebebasan. Sebuah fungsi bisa dibuat kapanpun, diberikan sebagai argumen kedalam fungsi lain, dan lalu dipanggil dari kode yang benar-benar berbeda nanti.

Kita sudah tahu bahwa sebuah fungsi bisa mengakses variabel diluar dari fungsi tersebut (variabel “luar”).

Tapi apa yang terjadi jika variabel luar berubah saat fungsinya dibuat? Akankan fungsinya mendapatkan nilai yang baru atau yang lama?

Dan bagaimana jika sebuah fungsi diberikan sebagai paramter dan dipanggil dibagian kode lain, akankah itu mendapatkan akses ke variabel luar ditempat itu?

Ayo kita peruas pengetahuan kita untuk mengerti skenario ini dan skenario yang lebih kompleks.

Kita akan bahas tentang variabel let/const di sini

Di JavaScript, ada 3 cara mendeklarasi variabel: let, const (cara-cara modern), dan var (sisa masa lalu).

  • Di artikel ini kita akan memakai variabel let dalam contoh.
  • Variabel, yang dideklarasi dengan const, bertindak sama, jadi artikel ini juga tentang const.
  • var usang punya perbedaan mencolok, mereka akan dibahas di artikel Si Tua "var".

Blok kode

Jika variabel dideklarasi di dalam blok kode {...}, ia hanya terlihat di dalam blok itu.

Misalnya:

{
  // lakukan pekerjaan dengan variabel lokal yang harusnya tak terlihat dari luar

  let message = "Hello"; // hanya terlihat dalam blok ini

  alert(message); // Hello
}

alert(message); // Error: message is not defined

Kita bisa memakai ini untuk mengisolasi potongan kode yang melakukan tugasnya sendiri, dengan variabel yang dia punya sendiri:

{
  // tampilkan pesan
  let message = "Hello";
  alert(message);
}

{
  // tampilkan pesan lain
  let message = "Goodbye";
  alert(message);
}
Akan muncul galat tanpa blok

Tolong ingat, tanpa blok terpisah akan muncul galat, jika kita memakai let dengan nama variabel yang sudah ada:

// tampilkan pesan
let message = "Hello";
alert(message);

// tampilkan pesan lain
let message = "Goodbye"; // Galat: variabel sudah dideklarasi
alert(message);

Untuk if, for, while dan lain-lain, variabel yang dideklarasi dalam {...} juga hanya terlihat di situ saja:

if (true) {
  let phrase = "Hello!";

  alert(phrase); // Hello!
}

alert(phrase); // Galat, variabel ini tak ada!

Di sini, setelah if selesai, alert di bawah tak akan melihat phrase, sehingga terjadi galat.

Ini keren, karena ia memperbolehkan kita membuat variabel blok-lokal, yang spesifik ke cabang if.

Hal serupa juga berlaku untuk loop for dan while:

for (let i = 0; i < 3; i++) {
  // variabel i hanya terlihat di dalam for ini
  alert(i); // 0, lalu 1, lalu 2
}

alert(i); // Galat, variabel ini tak ada

Visually, let i is outside of {...}. But the for construct is special here: the variable, declared inside it, is considered a part of the block.

Fungsi bersarang

Sebuah fungsi dikatakan “bersarang” apabila fungsi tersebut dibuat di dalam fungsi lainnya.

Hal tersebut mudah untuk dilakukan di JavaScript.

Kita dapat melakukannya untuk mengatur kode kita, seperti ini:

function sayHiBye(firstName, lastName) {

  // fungsi pembantu untuk digunakan di bawah
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

Di sini fungsi bersarang dibuat untuk kemudahan. Fungsi tersebut bisa mengakses variabel luar sehingga dapat mengembalikan nama lengkap. Fungsi bersarang cukup sering ditemui di JavaScript.

Yang lebih menarik yaitu, fungsi bersarang dapat dikembalikan: bisa sebagai properti dari objek baru atau sebagai nilai kembalian itu sendiri. Nilai kembalian tersebut bisa dipakai di tempat lain. Tak peduli di mana, ia masih punya akses ke variabel luar yang sama.

Di bawah ini, makeCounter membuat fungsi “counter” yang mengembalikan angka berikutnya di tiap invokasi:

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

Meski sederhana, varian kode itu yang sedikit dimodifikasi punya kegunaan praktis, misalnya, sebagai generator angka random untuk menggenerate nilai random untuk tes terotomasi.

How does this work? If we create multiple counters, will they be independent? What’s going on with the variables here?

Memahami hal begini bagus untuk pengetahuan keseluruhan JavaScript dan menguntungkan untuk skenario yang lebih komplex. Jadi ayo kita selami lebih dalam.

Lingkungan Lexikal

Sini jadilah naga!

Penjelasan teknikal mendalam ada di depan.

Semakin jauh aku menghindari detil bahasa level-rendah, pemahaman apapun tanpa mereka akan kekurangan dan tak-lengkap, jadi bersiaplah.

Supaya jelas, penjelasan dibagi dalam beberapa langkah.

Langkah 1. Variabel

Di JavaScript, setiap fungsi yang berjalan, blok kode {...}, dan satu script yang menyeluruh punya objek internal (tersembunyi) yang terasosiasi yang dikenal dengan Lingkungan Lexikal.

Objek Lingkungan Lexikal punya dua bagian:

  1. Rekaman Lingkungan – objek yang menyimpan semua variabel lokal sebagai propertinya (dan beberapa informasi lain seperti nilai this).
  2. Referensi ke lingkungan lexikal luar, yang terasosiasi dengan kode luar.

"Variabel" cuma suatu properti dari objek internal spesial, Rekaman Lingkungan. “Untuk memperoleh atau mengganti variabel” berarti “memperoleh atau mengganti properti dari objek itu”.

Di kode sederhana tanpa fungsi ini, cuma ada satu Lingkugan Lexikal:

Ini yang disebut Lingkungan Lexikal global, terasosiasi dengan script keseluruhan.

Di gambar di atas, kotak persegi panjang artinya Rekaman Lingkungan (simpanan variabel) dan panah artinya referensi luar. Lingkungan Lexikal global tak punya referensi luar, itulah kenapa panahnya menunjuk ke null.

Seiring kodenya mulai bereksekusi dan berjalan, Lingkungan Lexikal berganti.

Ini kode yang sedikit lebih panjang:

Kotak persegi panjang di sisi kanan mendemonstrasikan bagaimana Lingkungan Lexikal global berganti selama exekusi:

  1. Ketika script berjalan, Lingkungan Lexikal di-pre-populasi dengan semua variabel yang terdeklarasi.
    • Awalnya, mereka di state “Belum terinisialisir”. Itu state internal spesial, yang berarti bahwa engine tahu tentang variabelnya, tapi tak akan mengijinkan penggunaan itu sebelum let. Ini hampir sama saja dengan variabel itu tak ada.
  2. Lalu definisi let phrase muncul. Tak ada penetapan dulu, jadi nilainya undefined. Kita sudah bisa pakai variabel ini di momen ini.
  3. phrase diberikan nilai.
  4. phrase mengganti nilai.

Apapun terlihat simpel untuk sekarang, ya kan?

  • Variabel ialah properti dari objek internal spesial, yang terasosiasi dengan blok/fungsi/script yang sedang berexekusi.
  • Bekerja dengan variabel sebenarnya bekerja dengan properti objek itu.
Lingkungan Lexikal merupakan objek spesifikasi

“Lingkungan Lexikal” ialah objek spesifikasi: ia cuma ada “secara teori” di spesifikasi bahasa untuk menjelaskan bagaimana cara ia bekerja. Kita tak bisa memperoleh objek ini di kode kita dan memanipulasinya langsung.

Engine JavaScript juga bisa mengoptimisasi itu, menghapus variabel yang tak dipakai untuk menghemat memory dan melakukan trik internal lainnya, selama kelakuan yang terlihat sesuai deskripsi.

Langkah 2. Deklarasi Fungsi

Fungsi juga berupa nilai, seperti variabel.

Bedanya ialah Deklarasi Fungsi terinisialisasi penuh secara instan.

Ketika Lingkungan Lexikal dibuat, Deklarasi Fungsi segera menjadi fungsi siap-pakai (tak seperti let, yang tak bisa dipakai hingga deklarasi).

Itulah kenapa kita bisa memakai fungsi, yang dideklarasi sebagai Deklarasi Fungsi, bahkan sebelum deklarasinya itu sendiri.

Misalnya, ini state awal dari Lingkungan Lexikal global ketika kita tambah satu fungsi:

Alaminya, kelakukan ini cuma berlaku pada Deklarasi Fungsi, bukan Expresi Fungsi di mana kita menetapkan fungsi ke variabel, seperti let say = function(name)....

Langkah 3. Lingkungan Lexikal dalam dan luar

Ketika satu fungsi berjalan, di awal panggilan, Lingkungan Lexikal tercipta otomatis untuk menyimpan variabel lokal dan parameter dari panggilannya.

Misalnya, untuk say("John"), ini seperti (exekusinya ada di baris tersebut, yang diberi label dengan panah):

Selama panggilan fungsi, kita punya dua Lingkungan Lexikal: dalam (untuk panggilan fungsi) dan luar (global):

  • Lingkungan Lexikal dalam berkorespondensi dengan exekusi say yang sedang berlangsung. Ia punya properti tunggal: name, argumen fungsi. Kita panggil say("John"), jadi nilai name adalah "John".
  • Lingkungan Lexikal luar ialah Lingkungan Lexikal global. Ia punya variabel phrase dan fungsinya itu sendiri.

Lingkungan Lexikal dalam punya referensi ke outer.

Ketika kode ingin mengakses variabel – Lingkungan Lexikal dalam ditelusuri pertama, lalu terluar, lalu yang lebih terluar dan berikutnya.

Jika variabel tak ditemukan di manapun, itu adalah galat dalam mode ketat (tanpa use strict, penetapan ke variabel yang tak pernah ada menciptakan satu variabel global, untuk kompatibilitas dengan kode usang).

Di contoh ini penelusuran terjadi seperti berikut:

  • Untuk variabel name, alert di dalam say mencarinya segera di dalam Lingkungan Lexikal dalam.
  • Ketika ia ingin mengakses phrase, maka tak ada phrase secara lokal, jadi ia mengikuti referensi ke Lingkungan Lexikal luar dan menemui itu di sana.

Langkah 4. Mengembalikan fungsi

Ayo kembali ke contoh makeCounter.

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

Di awal tiap panggilan makeCounter(), objek Lingkungan Lexikal baru dibuat, untuk menyimpan variabel untuk perjalanan makeCounter ini.

Jadi kita punya dua Lingkungan Lexikal bersarang, sama seperti contoh di atas:

Bedanya adalah, selama exekusi dari makeCounter(), fungsi kecil bersarang tercipta dari cuma satu baris: return count++. Kita tak menjalankan itu sekarang, cuma membuat.

Semua fungsi mengingat Lingkungan Lexikal di mana mereka dibuat. Teknisnya, tak ada sihir di sini: semua fungsi punya properti tersembunyi bernama [[Environment]], yang menyimpan referensi ke Lingkungan Lexikal di mana fungsi itu dibuat:

Jadi, counter.[[Environment]] punya referensi ke {count: 0} Lingkungan Lexikal. Itulah bagaimana fungsi mengingat di mana ia dibuat, tak peduli di mana ia dipanggil. Referensi [[Environment]] diset sekali dan selamanya saat kresi fungsi.

Lalu, saat counter() dipanggil, Lingkungan Lexikal baru dibuat untuk panggilan, dan referensi Lingkungan Lexikal luar-nya diambil dari counter.[[Environment]]:

Sekarang ketika kode di dalam counter() mencari variabel count, ia pertama memeriksa Lingkungan Lexikal miliknya sendiri (kosong, karena tak ada variabel lokal di sana), lalu Lingkungan Lexikal dari panggilan makeCounter() luar, di mana ia ditemukan dan berubah.

Variabel diperbarui di Lingkungan Lexikal di mana ia tinggal.

Ini state setelah exekusi:

Jika kita panggil counter() beberapa kali, variabel count akan meningkat ke 2, 3, dan seterusnya, at the same place.

Closure

Ada satu istilah pemrograman umum “closure”, yang sebaiknya diketahui developer secara umum.

Closure ialah fungsi yang mengingat variabel luarnya dan bisa mengakses mereka. Di beberapa bahasa, itu tak mungkin, atau satu fungsi harus ditulis dalam cara spesial untuk membuat ini terjadi. Tapi seperti yang dijelaskan di atas, di JavaScript, semua fungsi alaminya adalah closure (cuma ada satu pengecualian, akan dibahas di Sintaks "new Function").

Yaitu: mereka otomatis mengingat di mana mereka dibuat menggunakan property [[Environment]] tersembunyi, kemudian kdoe mereka bisa mengakses variabel luar.

Ketika dalam interview, frontend developer mendapat pertanyaan tentang “apa itu closure?”, jawaban valid yaitu definisi closure dan penjelesan bahwa semua fungsi di JavaScript adalah closure, dan mungkin sedikit kata-kata tentang detil teknis: properti [[Environment]] dan bagaimana Lingkungan Lexikal bekerja.

Koleksi sampah

Biasanya, Lingkungan Lexikal dihapus dengan semua variabel setelah panggilan fungsinya selesai. Ini karena tak ada referensi ke situ. Sebagai objek JavaScript apapun, ia cuma ditahan di memory selama ia dapat digapai.

…Tapi jika ada fungsi bersarang yang masih dapat digapai setelah akhir fungsi, maka ia punya properti [[Environment]] yang mereferensi lingkungan lexikal.

Dalam hal Lingkungan Lexikal masih bisa digapai meski setelah berakhirnya fungsi itu, ia tetap hidup.

Misalnya:

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g.[[Environment]] menyimpan referensi ke Lingkungan Lexikal
// dari panggilan f() yang sesuai

Tolong diperhatikan apabila f() dipanggil beberapa kali, dan fungsi kembaliannya disimpan, maka seluruh objek lingkungan leksikal akan disimpan di memori. Ketiga-tiganya pada kode di bawah:

function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// 3 fungsi di array, semuanya terhubung ke lingkungan leksikal
// dari setiap f() yang bersangkutan
let arr = [f(), f(), f()];

Sebuah objek lingkungan leksikal mati apabila sudah tidak dapat dicapai (sperti objek lainnya). Dengan kata lain, objek tersebut hidup selama masih ada setidaknya satu fungsi bersarang yang mengacunya.

Di kode berikut, setelah fungsi bersarang itu dihapus, Lingkungan Lexikal lingkupannya (serta value-nya) dibersihkan dari memori;

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // selama fungsi g tetap ada, nilainya tetap berada di memori

g = null; // ...dan sekarang memori dibersihkan

Optimalisasi kehidupan nyata

Seperti yang kita lihat, di teori selama sebuah fungsi masih hidup, seluruh variabel luarnya juga disimpan.

Tetapi di praktiknya, mesin JavaScript mencoba untuk mengoptimalkannya. Mereka menganalisis penggunaan variabel dan apabila sudah jelas bahwa variabel luar sudah tidak digunakan – mereka dihapus.

Sebuah efek samping yang penting di V8 (Chrome, Opera) adalah variabel akan tak dapat diakses saat debugging

Cobalah jalankan contoh di bawah di Chrome dengan Developer Tools.

Saat dihentikan, pada console coba ketikkan alert(value).

function f() {
  let value = Math.random();

  function g() {
    debugger; // di console: ketik alert(value); Variabel tak ditemukan!
  }

  return g;
}

let g = f();
g();

Seperti yang kita lihat – variabel tersebut tak ditemukan! Secara teori, variabel tersebut masih bisa diakses, tetapi mesin mengoptimalkannya.

Hal tersebut mungkin menyebabkan masalah debugging yang aneh (mungkin memakan waktu). Salah satunya – apabila kita mendapat variabel luar yang tak diharapkan:

let value = "Surprise!";

function f() {
  let value = "the closest value";

  function g() {
    debugger; // di console: ketik alert(value); Surprise!
  }

  return g;
}

let g = f();
g();

Fitur V8 ini baik untuk diketahui. Jika kamu melakukan debug memakai Chrome/Opera, cepat atau lambat kamu akan menemuinya.

Ini bukan bug di debugger, melainkan fitur spesial dari V8. Mungkin ini akan diganti suatu saat. Kamu bisa mengeceknya dengan menjalankan contoh di laman ini.

Tugas

Fungsi sayHi menggunakan nama variabel dari luar. Ketika fungsinya berjalan, nilai manakah yang akan digunakan?

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // apakah yang akan tampil: "John" atau "Pete"?

Situasi seperti itu adalah hal yang biasa didalam peramban dan pengembangan di bagian server. Sebuah fungsi mungkin sudah dijadwalkan untuk dieksekusi nanti daripada saat dibuat, untuk contoh setelah sebuah aksi user atau setelah me-request ke jaringan.

Jadi, pertanyaannya adalah: apakah nilai terakhir akan diambil?

Jawabannya adalah: Pete.

Sebuah fungsi mendapatkan variabel yang sekarang, nilai paling terbaru akan digunakan.

Nilai variabel lama tidak akan tersimpan dimanapun. Ketika sebuah fungsi menginginkan sebuah variabel, nilai terbaru akan diambil dari lingkungan leksikalnya atau dari luar.

Fungsi makeWorker dibawah membuat fungsi lainnya dan mengembalikannya. Fungsi baru itu bisa dipanggil dari manapun.

Akankah itu mempunyai akses ke variabel luar dari tempat pembuatannya, atau dari tempat pemanggilannya, atau keduanya?

function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

// pembuatan fungsi
let work = makeWorker();

// dipanggil
work(); // apakah yang akan tampil?

Nilai manakah yang akan muncul? “Pete” atau “John”?

Jawabannya adalah: Pete.

Fungsi work() didalam kode mendapatkan name dari tempat dimana itu dibuat daripada mereferensi dari luar lingkungannya :

jadi, hasilnya adalah "Pete" disini.

Tapi jika disana tidak ada let name didalam makeWorker(), maka pencarian akan berlanjut ke luar dan mengambil variabel global seperti yang bisa kita lihat diatas. Di kasus ini hasilnya akan menjadi "John".

Di sini kita membuat dua counter: counter dan counter2 menggunakan fungsi makeCounter yang sama.

Apakah mereka independen? Apa yang akan counter kedua munculkan? 0,1 atau 2,3 atau yang lainnya?

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?
alert( counter2() ); // ?

Jawaban: 0,1.

Fungsi counter dan counter2 dibuat dengan panggilan fungsi makeCounter yang berbeda.

Jadi mereka memiliki lingkungan leksikal yang berbeda, dengan count mereka masing-masing.

Disini kita memiliki objek counter yang dibuat dengan bantuan fungsi konstruktor.

Apakah hal tersebut akan bekerja? Apa yang akan muncul?

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };
  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?

Tentu saja hal tersebut akan bekerja.

Kedua fungsi bersarang dibuat dengan lingkungan leksikal yang sama, jadi mereka membagi akses ke variabel count yang sama:

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };

  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1

Lihatlah kode di bawah ini. Apa hasil dari panggilan fungsi di baris terakhir?

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();

Hasilnya yaitu sebuah error.

Fungsi sayHi dideklarasikan di dalam if, jadi fungsi tersebut hanya hidup di dalamnya. Tidak ada fungsi sayHi di luar.

Buatlah sebuah fungsi sum yang bekerja seperti ini: sum(a)(b) = a+b.

Ya, seperti ini, dengan kurung ganda (bukan salah ketik).

Sebagai contoh:

sum(1)(2) = 3
sum(5)(-1) = 4

Agar kurung kedua berhasil, yang pertama harus mengembalikan sebuah fungsi.

Seperti ini:

function sum(a) {

  return function(b) {
    return a + b; // mengambil "a" dari lingkungan leksikal luar
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4

Apakah hasil dari kode ini?

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

P.S. There’s a pitfall in this task. The solution is not obvious. Catatan. Terdapat jebakan pada task ini. Solusinya menjadi kurang jelas.

Hasilnya adalah: error.

Cobalah jalankan ini:

let x = 1;

function func() {
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 2;
}

func();

Didalam contoh ini kita bisa mengamati perbedaan aneh diantara variabel yang “tidak-ada” dan “belum diinisialisasi”.

Mungkin seperti yang telah kamu baca didalam artikel Lingkup variabel, closure, sebuah variabel dimulai didalam state “belum diinisialisasi” sejak saat eksekusinya memasuki blok kode (atau sebuah fungsi). Dan itu akan tetap belum diinisialisasi sampai statemen let yang bersangkutan.

Dengan kata lain, sebuah variabel secara teknis ada, tapi kita belum bisa menggunakannya sebelum let.

Kode diatas mendemonstrasikan hal itu.

function func() {
  // variabel lokal x dikenal mesinnya di awal dari fungsinya,
  // tapi "belum diinisialisasi" (tidak dapat digunakan) sampai let ("zona mati")
  // karenanya terdapat error

  console.log(x); // ReferenceError: Cannot access 'x' before initialization

  let x = 2;
}

Zona tidak terpakai dari sebuah variabel ini (dari awal blok kode sampai let) terkadang dipanggil dengan “zona mati”.

Kita memiliki method bawaan arr.filter(f) untuk array. Method tersebut menyaring seluruh elemen menggunakan fungsi f. Apabila mengembalikan true, maka elemen tersebut dikembalikan di array hasil.

Buatlah filter “yang siap pakai”:

  • inBetween(a, b) – antara a dan b atau sama dengan (inklusif).
  • inArray([...]) – terkandung di dalam array.

Penggunaannya harus seperti ini:

  • arr.filter(inBetween(3,6)) – menyimpan hanya nilai di antara 3 dan 6.
  • arr.filter(inArray([1,2,3])) – menyimpan elemen apabila sama dengan salah satu dari [1,2,3].

Sebagai contoh:

/* .. implementasi inBetween dan inArray */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

Buka sandbox dengan tes.

Filter inBetween

function inBetween(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

Filter inArray

function inArray(arr) {
  return function(x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

Buka solusi dengan tes di sandbox.

Kita memiliki array objek untuk diurutkan:

let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" }
];

Cara yang biasa dilakukan yaitu:

// berdasarkan name (Ann, John, Pete)
users.sort((a, b) => a.name > b.name ? 1 : -1);

// berdasarkan age (Pete, Ann, John)
users.sort((a, b) => a.age > b.age ? 1 : -1);

Apakah kita dapat membuatnya lebih ringkas, seperti ini?

users.sort(byField('name'));
users.sort(byField('age'));

Jadi, daripada menulis sebuah fungsi, cukup tulis byField(fieldName).

Tulislah fungsi byField yang dapat digunakan untuk itu.

Buka sandbox dengan tes.

function byField(fieldName){
  return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
}

Buka solusi dengan tes di sandbox.

Kode berikut membuat array dari shooters.

Setiap fungsi diinginkan untuk mengeluarkan angkanya sendiri. Tetapi ada yang salah…

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // fungsi shooter
      alert( i ); // seharusnya mengeluarkan angkanya sendiri
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// semua penembak menunjukkan 10 bukannya angka mereka 0, 1, 2, 3...
army[0](); // 10 dari nomor penembak 0
army[1](); // 10 dari penembak nomor 1
army[2](); // 10 ...dan seterusnya.

Mengapa semua penembak menunjukkan nilai yang sama?

Perbaiki kode agar berfungsi sebagaimana mestinya.

Buka sandbox dengan tes.

Let’s examine what exactly happens inside makeArmy, and the solution will become obvious.

  1. It creates an empty array shooters:

    let shooters = [];
  2. Fills it with functions via shooters.push(function) in the loop.

    Every element is a function, so the resulting array looks like this:

    shooters = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. The array is returned from the function.

    Then, later, the call to any member, e.g. army[5]() will get the element army[5] from the array (which is a function) and calls it.

    Now why do all such functions show the same value, 10?

    That’s because there’s no local variable i inside shooter functions. When such a function is called, it takes i from its outer lexical environment.

    Then, what will be the value of i?

    If we look at the source:

    function makeArmy() {
      ...
      let i = 0;
      while (i < 10) {
        let shooter = function() { // shooter function
          alert( i ); // should show its number
        };
        shooters.push(shooter); // add function to the array
        i++;
      }
      ...
    }

    We can see that all shooter functions are created in the lexical environment of makeArmy() function. But when army[5]() is called, makeArmy has already finished its job, and the final value of i is 10 (while stops at i=10).

    As the result, all shooter functions get the same value from the outer lexical environment and that is, the last value, i=10.

    As you can see above, on each iteration of a while {...} block, a new lexical environment is created. So, to fix this, we can copy the value of i into a variable within the while {...} block, like this:

    function makeArmy() {
      let shooters = [];
    
      let i = 0;
      while (i < 10) {
          let j = i;
          let shooter = function() { // shooter function
            alert( j ); // should show its number
          };
        shooters.push(shooter);
        i++;
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    // Now the code works correctly
    army[0](); // 0
    army[5](); // 5

    Here let j = i declares an “iteration-local” variable j and copies i into it. Primitives are copied “by value”, so we actually get an independent copy of i, belonging to the current loop iteration.

    The shooters work correctly, because the value of i now lives a little bit closer. Not in makeArmy() Lexical Environment, but in the Lexical Environment that corresponds to the current loop iteration:

    Such a problem could also be avoided if we used for in the beginning, like this:

    function makeArmy() {
    
      let shooters = [];
    
      for(let i = 0; i < 10; i++) {
        let shooter = function() { // shooter function
          alert( i ); // should show its number
        };
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    army[0](); // 0
    army[5](); // 5

    That’s essentially the same, because for on each iteration generates a new lexical environment, with its own variable i. So shooter generated in every iteration references its own i, from that very iteration.

Now, as you’ve put so much effort into reading this, and the final recipe is so simple – just use for, you may wonder – was it worth that?

Well, if you could easily answer the question, you wouldn’t read the solution. So, hopefully this task must have helped you to understand things a bit better.

Besides, there are indeed cases when one prefers while to for, and other scenarios, where such problems are real.

Buka solusi dengan tes di sandbox.

Peta tutorial

komentar

baca ini sebelum berkomentar…
  • Jika Anda memiliki saran apa yang harus ditingkatkan - silakan kunjungi kirimkan Github issue atau pull request sebagai gantinya berkomentar.
  • Jika Anda tidak dapat memahami sesuatu dalam artikel – harap jelaskan.
  • Untuk menyisipkan beberapa kata kode, gunakan tag <code>, untuk beberapa baris – bungkus dengan tag <pre>, untuk lebih dari 10 baris – gunakan sandbox (plnkr, jsbin, < a href='http://codepen.io'>codepen…)