kodingup.camp

Beranda/JavaScript Mastery: Fundamentals

Pola Fungsional Lanjutan

Modul 15 dari 164 menit bacaAdvanced functional patterns

Isi modul

    Kenapa ini penting

    Di JavaScript, function adalah first-class citizen. Artinya function bisa disimpan di variabel, dikirim sebagai argument ke function lain, dan dikembalikan sebagai hasil function. Kemampuan ini membuka pola-pola yang sangat kuat dan dipakai di mana-mana: event klik tombol (callback), pengolahan data (map, filter, reduce), function yang "mengingat" data (closure), dan penyelesaian masalah bercabang seperti folder di dalam folder (rekursi). Kalau nanti kamu belajar React atau Node.js, pola-pola ini muncul di hampir setiap file.

    Konsep inti

    Callback

    Callback adalah function yang dikirim sebagai argument ke function lain, untuk dipanggil nanti oleh function penerima. Analogi: kamu memesan makanan di restoran dan meninggalkan nomor HP. Restoran yang akan "memanggil balik" (call back) saat makanan siap. Kamu tidak perlu menunggu di depan dapur.

    Contoh nyata callback: setTimeout(fn, 1000) memanggil fn setelah 1 detik, tombol.addEventListener("click", fn) memanggil fn saat tombol diklik, dan array.forEach(fn) memanggil fn untuk setiap elemen.

    Perhatikan bedanya: jalankan(sapa) mengirim function-nya, sedangkan jalankan(sapa()) mengirim hasil pemanggilannya.

    Higher-order function (HOF)

    Higher-order function adalah function yang menerima function sebagai argument, mengembalikan function, atau keduanya. setTimeout, forEach, map, filter, dan reduce semuanya HOF. HOF memisahkan "apa yang dilakukan" dari "bagaimana mengulangnya", sehingga kode lebih pendek dan maksudnya lebih jelas.

    Trio pengolah array: map, filter, reduce

    Ketiganya tidak mengubah array asli. Mereka menghasilkan nilai baru.

    Method Fungsi Callback mengembalikan Hasil
    map Mengubah setiap elemen Elemen baru Array baru, panjang sama
    filter Menyaring elemen true (simpan) / false (buang) Array baru, panjang sama atau lebih pendek
    reduce Menggabungkan semua elemen jadi satu nilai Akumulator baru Satu nilai (angka, object, array, dan sebagainya)

    Analogi dapur: kamu punya sekeranjang buah.

    reduce(callback, nilaiAwal): callback menerima (akumulator, elemen) dan mengembalikan akumulator baru. Selalu berikan nilai awal (misalnya 0 untuk penjumlahan). Tanpa nilai awal, reduce pada array kosong melempar error.

    Ketiganya bisa dirangkai (chaining): data.filter(...).map(...).reduce(...).

    Method HOF lain yang sering dipakai: forEach (jalankan sesuatu untuk setiap elemen, tanpa hasil), find (elemen pertama yang cocok), some (adakah yang cocok?), every (apakah semua cocok?), dan sort dengan function pembanding.

    Closure

    Closure terjadi ketika sebuah function "mengingat" variabel dari scope tempat ia dibuat, bahkan setelah function luarnya selesai dijalankan. Analogi: ransel. Function dalam membawa ransel berisi variabel dari lingkungan asalnya ke mana pun ia pergi.

    Kegunaan closure:

    Rekursi

    Rekursi adalah function yang memanggil dirinya sendiri untuk menyelesaikan versi masalah yang lebih kecil. Setiap rekursi wajib punya:

    1. Base case: kondisi berhenti yang langsung me-return tanpa memanggil diri sendiri.
    2. Recursive case: memanggil diri sendiri dengan masalah yang lebih kecil dan mendekati base case.

    Analogi: boneka matryoshka (boneka Rusia bertumpuk). Untuk menemukan boneka terkecil, buka boneka, lalu ulangi proses yang sama pada boneka di dalamnya, sampai menemukan boneka yang tidak bisa dibuka lagi (base case).

    Tanpa base case, function memanggil dirinya terus sampai memori tumpukan panggilan habis: RangeError: Maximum call stack size exceeded. Rekursi sangat cocok untuk data bertingkat seperti struktur folder, komentar berbalas, atau menu bertingkat.

    Contoh kode

    Callback dan higher-order function:

    // Callback sederhana
    function prosesPesanan(nama, callback) {
      console.log(`Memproses pesanan ${nama}...`);
      callback(nama);
    }
    function kirimNotif(nama) {
      console.log(`Notifikasi: pesanan ${nama} siap!`);
    }
    prosesPesanan("Kopi Susu", kirimNotif);
    // → Memproses pesanan Kopi Susu...
    // → Notifikasi: pesanan Kopi Susu siap!
    
    // Callback anonim dengan arrow
    prosesPesanan("Teh", (n) => console.log(`${n} diantar ke meja 3`));
    // → Memproses pesanan Teh...
    // → Teh diantar ke meja 3
    
    // setTimeout: callback dipanggil nanti
    setTimeout(() => console.log("Muncul setelah 1 detik"), 1000);
    console.log("Muncul duluan");
    // → Muncul duluan
    // → (1 detik kemudian) Muncul setelah 1 detik
    // Catatan: kalau seluruh blok ini dijalankan sekaligus, output "Langkah" di bawah
    // juga tercetak SEBELUM pesan setTimeout, karena setTimeout menunggu 1 detik.
    
    // HOF yang mengembalikan function
    function ulangi(n, aksi) {
      for (let i = 0; i < n; i++) aksi(i);
    }
    ulangi(3, (i) => console.log("Langkah", i + 1));
    // → Langkah 1, Langkah 2, Langkah 3
    

    map, filter, reduce, dan teman-temannya:

    const produk = [
      { nama: "Kaos", harga: 80000, stok: 12 },
      { nama: "Topi", harga: 45000, stok: 0 },
      { nama: "Jaket", harga: 250000, stok: 4 },
      { nama: "Kaus kaki", harga: 15000, stok: 30 },
    ];
    
    // map: ambil nama saja
    const daftarNama = produk.map((p) => p.nama);
    console.log(daftarNama); // → [ 'Kaos', 'Topi', 'Jaket', 'Kaus kaki' ]
    
    // map: harga setelah diskon 10%
    const hargaDiskon = produk.map((p) => ({ nama: p.nama, harga: p.harga * 0.9 }));
    console.log(hargaDiskon[0]); // → { nama: 'Kaos', harga: 72000 }
    
    // filter: hanya yang stoknya ada
    const tersedia = produk.filter((p) => p.stok > 0);
    console.log(tersedia.length); // → 3
    
    // reduce: total nilai stok gudang (harga × stok)
    const nilaiGudang = produk.reduce((total, p) => total + p.harga * p.stok, 0);
    console.log(nilaiGudang); // → 2410000
    
    // Chaining: total harga barang tersedia di bawah 100 ribu
    const totalMurah = produk
      .filter((p) => p.stok > 0 && p.harga < 100000)
      .map((p) => p.harga)
      .reduce((a, b) => a + b, 0);
    console.log(totalMurah); // → 95000
    
    // reduce untuk mengelompokkan
    const nilai = [90, 72, 85, 60, 95];
    const kelompok = nilai.reduce(
      (acc, n) => {
        if (n >= 75) acc.lulus.push(n);
        else acc.remedial.push(n);
        return acc;
      },
      { lulus: [], remedial: [] }
    );
    console.log(kelompok); // → { lulus: [ 90, 85, 95 ], remedial: [ 72, 60 ] }
    
    // find, some, every, sort
    console.log(produk.find((p) => p.harga > 100000).nama); // → Jaket
    console.log(produk.some((p) => p.stok === 0));          // → true
    console.log(produk.every((p) => p.harga > 10000));      // → true
    const termurah = [...produk].sort((a, b) => a.harga - b.harga);
    console.log(termurah.map((p) => p.nama)); // → [ 'Kaus kaki', 'Topi', 'Kaos', 'Jaket' ]
    
    // Array asli tidak berubah
    console.log(produk[0].nama); // → Kaos
    

    Closure dan rekursi:

    // Closure: penghitung dengan state pribadi
    function buatPenghitung() {
      let hitungan = 0; // tidak bisa diakses langsung dari luar
      return {
        tambah: () => ++hitungan,
        lihat: () => hitungan,
      };
    }
    const klik = buatPenghitung();
    klik.tambah();
    klik.tambah();
    console.log(klik.lihat()); // → 2
    console.log(klik.hitungan); // → undefined (benar-benar pribadi)
    
    const klikLain = buatPenghitung(); // closure baru, hitungan sendiri
    console.log(klikLain.lihat()); // → 0
    
    // Function factory
    const buatPengali = (faktor) => (x) => x * faktor;
    const kaliDua = buatPengali(2);
    const kaliSepuluh = buatPengali(10);
    console.log(kaliDua(7), kaliSepuluh(7)); // → 14 70
    
    // Rekursi: faktorial  n! = n × (n-1)!
    function faktorial(n) {
      if (n <= 1) return 1;          // base case
      return n * faktorial(n - 1);   // recursive case
    }
    console.log(faktorial(5)); // → 120  (5 × 4 × 3 × 2 × 1)
    
    // Rekursi pada data bertingkat: hitung total file di semua folder
    const folder = {
      nama: "proyek",
      file: 2,
      isi: [
        { nama: "src", file: 5, isi: [{ nama: "utils", file: 3, isi: [] }] },
        { nama: "docs", file: 1, isi: [] },
      ],
    };
    function totalFile(f) {
      let total = f.file;
      for (const sub of f.isi) {
        total += totalFile(sub); // panggil diri sendiri untuk subfolder
      }
      return total;
    }
    console.log(totalFile(folder)); // → 11
    

    Kesalahan umum

    1. Memanggil callback saat mengirimnya. setTimeout(sapa(), 1000) langsung menjalankan sapa dan mengirim hasilnya. Kirim function-nya: setTimeout(sapa, 1000).
    2. Lupa return di callback map/filter yang memakai { }. Hasilnya array berisi undefined atau array kosong.
    3. reduce tanpa nilai awal. Pada array kosong muncul TypeError: Reduce of empty array with no initial value. Selalu berikan nilai awal.
    4. Memakai map hanya untuk efek samping. Kalau tidak butuh array hasil, pakai forEach atau for...of.
    5. Rekursi tanpa base case atau masalah yang tidak mengecil. Muncul RangeError: Maximum call stack size exceeded. Pastikan setiap panggilan mendekati kondisi berhenti.
    6. sort() tanpa function pembanding untuk angka. [10, 9, 1].sort() menghasilkan [1, 10, 9] karena diurutkan sebagai teks. Gunakan (a, b) => a - b. Ingat juga bahwa sort mengubah array asli, jadi salin dulu dengan [...arr].

    Latihan

    1. Mudah, Dari [1, 2, 3, 4, 5, 6], buat array kuadrat dari bilangan genap saja memakai filter dan map. Hasil: [4, 16, 36].
    2. Sedang, Diberikan const transaksi = [{ jenis: "masuk", jumlah: 500000 }, { jenis: "keluar", jumlah: 120000 }, { jenis: "keluar", jumlah: 80000 }, { jenis: "masuk", jumlah: 200000 }]. Hitung saldo akhir dengan satu reduce (masuk menambah, keluar mengurangi).
    3. Menantang, (a) Buat closure buatDompet(saldoAwal) yang mengembalikan method setor(x), tarik(x) (tolak kalau saldo kurang, kembalikan pesan), dan cekSaldo(). (b) Buat function rekursif jumlahDigit(n) yang menjumlahkan digit sebuah bilangan bulat positif, misalnya jumlahDigit(1234) menghasilkan 10.
    Lihat solusi

    1.

    const hasil = [1, 2, 3, 4, 5, 6]
      .filter((n) => n % 2 === 0)
      .map((n) => n ** 2);
    console.log(hasil); // → [ 4, 16, 36 ]
    

    2.

    const transaksi = [
      { jenis: "masuk", jumlah: 500000 },
      { jenis: "keluar", jumlah: 120000 },
      { jenis: "keluar", jumlah: 80000 },
      { jenis: "masuk", jumlah: 200000 },
    ];
    const saldo = transaksi.reduce(
      (acc, t) => (t.jenis === "masuk" ? acc + t.jumlah : acc - t.jumlah),
      0
    );
    console.log(saldo); // → 500000
    

    3.

    // (a) Closure dompet
    function buatDompet(saldoAwal) {
      let saldo = saldoAwal;
      return {
        setor(x) {
          saldo += x;
          return saldo;
        },
        tarik(x) {
          if (x > saldo) return "Saldo tidak cukup";
          saldo -= x;
          return saldo;
        },
        cekSaldo: () => saldo,
      };
    }
    const dompet = buatDompet(100000);
    console.log(dompet.setor(50000));  // → 150000
    console.log(dompet.tarik(200000)); // → Saldo tidak cukup
    console.log(dompet.tarik(30000));  // → 120000
    console.log(dompet.cekSaldo());    // → 120000
    
    // (b) Rekursi jumlah digit
    function jumlahDigit(n) {
      if (n < 10) return n;                         // base case: satu digit
      return (n % 10) + jumlahDigit(Math.floor(n / 10)); // digit terakhir + sisanya
    }
    console.log(jumlahDigit(1234)); // → 10
    console.log(jumlahDigit(9));    // → 9
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.