JavaScript

Belum masuk. Progresmu tidak tersimpan.

Masuk
  • Bab 11 dari 12
  • 35 menit
  • 1 latihan kode
  • 4 soal kuis

Promise, Generator, dan Struktur Data

Di bab ini (9 bagian)
  1. Tujuan
  2. Kenapa harus asynchronous?
  3. Promise: janji yang pasti ditepati atau dibatalkan
  4. Iterator dan generator
  5. Map dan Set
  6. Symbol dan for...of
  7. Proxy dan Reflect, sekilas
  8. Kesalahan umum
  9. Rangkuman

Tujuan bab ini

  • Memahami kenapa JavaScript butuh kode asynchronous dan apa itu callback hell.
  • Membuat dan merangkai Promise, serta memilih antara Promise.all, allSettled, race, dan any.
  • Membuat iterator sendiri dengan generator (function* dan yield).
  • Memakai Map, Set, Symbol, dan for...of, plus mengenal WeakMap, WeakSet, Proxy, dan Reflect.

Kenapa harus asynchronous?

JavaScript berjalan di satu thread. Bayangkan kasir tunggal di kafe. Kalau kasir menunggu kopi selesai diseduh sebelum melayani pembeli berikutnya, antrean macet. Kasir yang pintar mencatat pesanan, memberi nomor antrean, lalu lanjut melayani orang lain. Saat kopi siap, nomormu dipanggil.

Operasi lambat seperti mengambil data dari server atau membaca file diperlakukan sama: JavaScript menitipkannya, lanjut mengerjakan kode lain, lalu kembali saat hasilnya siap.

console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
// A
// C
// B

Walau jedanya 0 ms, B tetap muncul terakhir karena callback timer baru dijalankan setelah kode yang sedang berjalan selesai.

Cara lama menangani hasil asynchronous adalah callback: fungsi yang dititipkan untuk dipanggil nanti. Masalahnya muncul kalau langkahnya berurutan.

ambilUser(1, (user) => {
  ambilPost(user.id, (post) => {
    ambilKomentar(post.id, (komentar) => {
      tampilkan(komentar); // makin menjorok ke kanan...
    });
  });
});

Bentuk piramida ini dijuluki callback hell: susah dibaca, dan penanganan error harus ditulis ulang di setiap tingkat.

Promise: janji yang pasti ditepati atau dibatalkan

Promise (ES2015) adalah objek yang mewakili hasil yang belum ada sekarang. Ia punya tiga state:

StateArti
pendingmasih berjalan, belum ada hasil
fulfilledberhasil, membawa sebuah nilai
rejectedgagal, membawa alasan (biasanya Error)

Begitu fulfilled atau rejected, state-nya terkunci dan tidak bisa berubah lagi.

function cekStok(jumlah) {
  return new Promise((resolve, reject) => {
    if (jumlah <= 5) resolve(`Stok cukup untuk ${jumlah}`);
    else reject(new Error("Stok habis"));
  });
}

cekStok(3)
  .then((pesan) => console.log(pesan))   // Stok cukup untuk 3
  .catch((err) => console.log(err.message))
  .finally(() => console.log("Selesai cek")); // selalu jalan
  • .then(fn) dipanggil saat fulfilled.
  • .catch(fn) dipanggil saat rejected, termasuk kalau ada error dilempar di .then sebelumnya.
  • .finally(fn) (ES2018) selalu jalan, cocok untuk menyembunyikan loading spinner.

Chaining

Kuncinya: .then() selalu mengembalikan Promise baru. Nilai yang kamu return di dalamnya menjadi input .then() berikutnya. Kalau yang di-return adalah Promise, rantai menunggu Promise itu selesai dulu.

Promise.resolve(2)
  .then((x) => x * 10)
  .then((x) => Promise.resolve(x + 1))
  .then((x) => console.log(x)); // 21

Piramida callback tadi jadi rata:

ambilUser(1)
  .then((user) => ambilPost(user.id))
  .then((post) => ambilKomentar(post.id))
  .then(tampilkan)
  .catch((err) => console.log("Gagal:", err.message)); // satu catch untuk semua

Menjalankan banyak Promise sekaligus

MethodSelesai saatGagal saat
Promise.all (ES2015)semua fulfilled, hasilnya arraysalah satu rejected
Promise.allSettled (ES2020)semua selesai, apa pun hasilnyatidak pernah reject
Promise.race (ES2015)yang pertama selesai, berhasil atau gagalyang pertama ternyata rejected
Promise.any (ES2021)yang pertama fulfilledsemua rejected (AggregateError)
const tugas = [Promise.resolve("a"), Promise.reject(new Error("x")), Promise.resolve("c")];

Promise.allSettled(tugas).then((hasil) => console.log(hasil.map((h) => h.status)));
// [ 'fulfilled', 'rejected', 'fulfilled' ]

Promise.any(tugas).then((v) => console.log(v)); // a

Tiap item hasil allSettled berbentuk { status: "fulfilled", value } atau { status: "rejected", reason }.

Iterator dan generator

Iterator adalah objek dengan method next() yang mengembalikan { value, done }. Objek yang bisa memberikan iterator lewat [Symbol.iterator]() disebut iterable: array, string, Map, dan Set termasuk.

const it = ["a", "b"][Symbol.iterator]();
console.log(it.next()); // { value: 'a', done: false }
console.log(it.next()); // { value: 'b', done: false }
console.log(it.next()); // { value: undefined, done: true }

Menulis iterator manual itu repot. Generator (function*) membuatnya gampang. Setiap yield menyerahkan satu nilai lalu menjeda fungsi sampai next() berikutnya dipanggil.

function* hitungMundur(n) {
  while (n > 0) {
    yield n;
    n--;
  }
}

console.log([...hitungMundur(3)]); // [ 3, 2, 1 ]

Karena nilainya dibuat sesuai permintaan (lazy), generator bahkan boleh tak terbatas, asal kamu berhenti mengambil di suatu titik.

Map dan Set

Map menyimpan pasangan key-value seperti objek, tapi key-nya boleh tipe apa saja, urutannya terjaga, dan ukurannya langsung ada di .size.

const nilai = new Map();
nilai.set("Dini", 90).set(1, "satu");
console.log(nilai.get("Dini")); // 90
console.log(nilai.has(1));      // true
console.log(nilai.size);        // 2

Set menyimpan nilai unik. Trik paling populer: membuang duplikat array.

const unik = [...new Set([1, 2, 2, 3, 3])];
console.log(unik); // [ 1, 2, 3 ]

WeakMap dan WeakSet adalah versi "lemah": key-nya harus objek dan tidak menahan objek itu dari garbage collector. Begitu objek tidak dipakai di tempat lain, entrinya ikut hilang. Konsekuensinya, keduanya tidak punya .size dan tidak bisa di-loop. Cocok untuk menempelkan data tambahan ke objek, misalnya elemen DOM, tanpa risiko memory leak.

Symbol dan for...of

Symbol() adalah tipe primitif yang selalu unik, bahkan dengan deskripsi sama. Biasanya dipakai sebagai key properti yang dijamin tidak bentrok.

const id = Symbol("id");
const user = { nama: "Andi", [id]: 77 };
console.log(Symbol("id") === Symbol("id")); // false
console.log(Object.keys(user));             // [ 'nama' ], symbol tidak ikut

for...of menelusuri nilai dari iterable apa pun. Bedakan dengan for...in yang menelusuri key objek.

for (const [nama, skor] of nilai) {
  console.log(nama, skor); // Dini 90, lalu 1 satu
}

Proxy dan Reflect, sekilas

Proxy membungkus objek untuk menyadap operasi seperti baca dan tulis properti. Reflect menyediakan versi "normal" operasi itu, jadi kamu bisa menambah perilaku lalu meneruskan ke aslinya.

const aman = new Proxy({}, {
  get(obj, prop) {
    return Reflect.has(obj, prop) ? Reflect.get(obj, prop) : "tidak ada";
  },
});
aman.nama = "Wulan";
console.log(aman.nama);  // Wulan
console.log(aman.umur);  // tidak ada

Framework seperti Vue 3 memakai Proxy untuk mendeteksi perubahan data secara otomatis.

Kesalahan umum

  • Lupa return di dalam .then(). Rantai berikutnya menerima undefined dan tidak menunggu Promise yang kamu buat.
  • Tidak menulis .catch(). Promise yang rejected tanpa penanganan memunculkan peringatan "Unhandled promise rejection".
  • Memakai Promise.all saat sebagian boleh gagal. Satu kegagalan membuang semua hasil; pakai allSettled.
  • Memanggil generator lalu mengira hasilnya nilai. hitungMundur(3) mengembalikan objek generator; ambil nilainya lewat next(), spread, atau for...of.
  • Mengakses Map seperti objek. nilai["Dini"] bukan cara membaca Map; pakai nilai.get("Dini").
  • Memakai for...of pada objek biasa. Objek {} tidak iterable; pakai Object.entries(obj) dulu.

Rangkuman

  • Asynchronous membuat satu thread tetap responsif; callback bertingkat diganti Promise yang bisa dirangkai dengan .then, .catch, .finally.
  • all butuh semua berhasil, allSettled menunggu semua apa pun hasilnya, race ambil yang tercepat, any ambil yang pertama berhasil.
  • Generator function* dengan yield adalah cara ringkas membuat iterator yang lazy.
  • Map untuk key-value dengan key bebas, Set untuk nilai unik, Symbol untuk key unik, for...of untuk menelusuri nilai iterable, dan Proxy/Reflect untuk menyadap operasi objek.

Selesaikan bab ini

  1. 1. Baca materi
  2. 2. Latihan kode (1)
  3. 3. Kuis (4 soal)

Latihan kode: Hasil aman, rentang, dan hitung kata

Lengkapi tiga fungsi:

  • async function ambilAman(daftar) menerima array Promise dan mengembalikan array nilainya. Promise yang gagal diganti null. Gunakan Promise.allSettled. Contoh: untuk [Promise.resolve(1), Promise.reject(new Error("x"))] hasilnya [1, null].
  • function* rentang(awal, akhir, langkah = 1) adalah generator yang meng-yield angka dari awal sampai akhir (termasuk) dengan lompatan langkah. Contoh: [...rentang(1, 10, 3)] menghasilkan [1, 4, 7, 10].
  • hitungKata(teks) mengembalikan Map berisi kata (dipisah spasi) dan jumlah kemunculannya. Contoh: hitungKata("a b a").get("a") menghasilkan 2.

Cek pemahaman

  1. 1Urutan apa yang ditampilkan kode berikut?

    console.log("A");
    setTimeout(() => console.log("B"), 0);
    Promise.resolve().then(() => console.log("C"));
    console.log("D");
  2. 2Kamu memuat tiga widget dan ingin tetap menampilkan yang berhasil walau satu gagal. Method yang paling cocok?

  3. 3Apa yang ditampilkan kode berikut?

    function* angka() {
      yield 1;
      yield 2;
      yield 2;
    }
    const s = new Set(angka());
    console.log(s.size);
  4. 4WeakMap bisa ditelusuri dengan `for...of` dan punya properti `.size`.