Bab 5 dari Eloquent JavaScript. Sebelumnya: 04 Struktur Data Objek dan Array. Berikutnya: 06 Kehidupan Rahasia Objek.

Ada dua cara nyusun desain software: Satu cara adalah bikinnya sesederhana mungkin sampai jelas gak ada kekurangannya, dan cara lainnya adalah bikinnya serumit mungkin sampai gak ada kekurangan yang jelas.

— C.A.R. Hoare, 1980 ACM Turing Award Lecture

Ilustrasi huruf dan hieroglif dari berbagai sistem tulisan: Latin, Yunani, Arab, Mesir kuno, dan lainnya

Program yang besar itu program yang mahal, dan bukan cuma karena waktu yang dibutuhin buat ngebangunnya. Ukuran hampir selalu melibatkan kerumitan, dan kerumitan bikin programmer bingung. Programmer yang bingung, pada gilirannya, ngenalin kesalahan (bug) ke program. Program yang besar lalu nyediain banyak ruang buat bug-bug ini bersembunyi, bikin mereka susah ditemuin.

Kita balik sebentar ke dua program contoh terakhir di pengenalan. Yang pertama berdiri sendiri dan panjangnya enam baris.

let total = 0, count = 1;
while (count <= 10) {
  total += count;
  count += 1;
}
console.log(total);

Yang kedua bergantung pada dua fungsi eksternal dan panjangnya satu baris.

console.log(sum(range(1, 10)));

Mana yang lebih mungkin berisi bug?

Kalau kita ngitung ukuran definisi sum dan range, program kedua juga besar, bahkan lebih besar dari yang pertama. Tapi tetap, saya berpendapat dia lebih mungkin benar.

Ini karena solusinya diekspresikan dalam kosakata yang sesuai dengan masalah yang diselesaiin. Ngejumlahin rentang angka bukan soal loop dan counter. Ini soal rentang dan jumlah.

Definisi kosakata ini (fungsi sum dan range) masih bakal melibatkan loop, counter, dan detail-detail insidental lainnya. Tapi karena mereka ngekspresiin konsep yang lebih sederhana daripada program secara keseluruhan, mereka lebih gampang dibikin benar.

Abstraksi

Dalam konteks pemrograman, kosakata jenis ini biasanya disebut abstraksi. Abstraksi ngasih kita kemampuan buat ngomongin masalah di level yang lebih tinggi (atau lebih abstrak), tanpa teralihkan sama detail-detail yang gak menarik.

Sebagai analogi, bandingin dua resep sup kacang polong ini. Yang pertama kaya gini:

Taruh 1 cangkir kacang polong kering per orang ke dalam wadah. Tambahin air sampai kacangnya terendam. Biarin kacangnya di air minimal 12 jam. Ambil kacangnya dari air dan taruh di panci. Tambahin 4 cangkir air per orang. Tutup pancinya dan biarin kacangnya mendidih pelan selama dua jam. Ambil setengah bawang bombay per orang. Potong-potong pakai pisau. Tambahin ke kacangnya. Ambil satu batang seledri per orang. Potong-potong pakai pisau. Tambahin ke kacangnya. Ambil satu wortel per orang. Potong-potong. Pakai pisau! Tambahin ke kacangnya. Masak 10 menit lagi.

Dan ini resep kedua:

Per orang: 1 cangkir kacang polong kering belah, 4 cangkir air, setengah bawang bombay cincang, satu batang seledri, dan satu wortel.

Rendam kacang 12 jam. Didihkan pelan 2 jam. Cincang dan tambahkan sayuran. Masak 10 menit lagi.

Yang kedua lebih pendek dan lebih gampang ditafsirin. Tapi kamu emang perlu paham beberapa kata terkait memasak kaya rendam, didihkan pelan, cincang, dan, saya rasa, sayuran.

Waktu memprogram, kita gak bisa ngandelin semua kata yang kita butuhin udah nunggu di kamus. Jadi, kita mungkin jatuh ke pola resep pertama, ngerjain langkah-langkah presisi yang harus dilakuin komputer, satu per satu, buta terhadap konsep level lebih tinggi yang mereka ekspresikan.

Ini keahlian yang berguna, di pemrograman, buat nyadar waktu kamu lagi kerja di level abstraksi yang terlalu rendah.

Mengabstraksi Pengulangan

Fungsi biasa, kaya yang udah kita lihat sejauh ini, adalah cara bagus buat ngebangun abstraksi. Tapi kadang mereka kurang.

Umum buat program ngelakuin sesuatu sejumlah kali tertentu. Kamu bisa nulis loop for buat itu, kaya gini:

for (let i = 0; i < 10; i++) {
  console.log(i);
}

Bisa kita abstraksi “ngelakuin sesuatu N kali” sebagai fungsi? Nah, gampang buat nulis fungsi yang manggil console.log N kali.

function repeatLog(n) {
  for (let i = 0; i < n; i++) {
    console.log(i);
  }
}

Tapi gimana kalau kita mau ngelakuin sesuatu selain nge-log angka? Karena “ngelakuin sesuatu” bisa diwakili sebagai fungsi dan fungsi cuma nilai, kita bisa ngasih aksi kita sebagai nilai fungsi.

function repeat(n, action) {
  for (let i = 0; i < n; i++) {
    action(i);
  }
}
 
repeat(3, console.log);
// → 0
// → 1
// → 2

Kita gak harus ngasih fungsi yang udah didefinisikan ke repeat. Sering kali, lebih gampang bikin nilai fungsi di tempat.

let labels = [];
repeat(5, i => {
  labels.push(`Unit ${i + 1}`);
});
console.log(labels);
// → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]

Ini terstruktur agak mirip loop for, dia pertama ngejelasin jenis loop-nya lalu nyediain badannya. Tapi, badannya sekarang ditulis sebagai nilai fungsi, yang dibungkus di dalam tanda kurung pemanggilan repeat. Makanya dia harus ditutup dengan kurung kurawal penutup dan tanda kurung penutup. Di kasus kaya contoh ini, di mana badannya cuma satu ekspresi kecil, kamu juga bisa ngilangin kurung kurawalnya dan nulis loop-nya di satu baris.

Higher-Order Functions

Fungsi yang beroperasi pada fungsi lain, baik dengan ngambilnya sebagai argumen atau dengan ngembaliinnya, disebut higher-order functions (fungsi tingkat tinggi). Karena kita udah lihat bahwa fungsi itu nilai biasa, gak ada yang terlalu luar biasa soal fakta bahwa fungsi kaya gitu ada. Istilahnya datang dari matematika, di mana perbedaan antara fungsi dan nilai lain dianggap lebih serius.

Higher-order functions ngebolehin kita mengabstraksi aksi, bukan cuma nilai. Mereka datang dalam beberapa bentuk. Misalnya, kita bisa punya fungsi yang bikin fungsi baru.

function greaterThan(n) {
  return m => m > n;
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true

Kita juga bisa punya fungsi yang ngubah fungsi lain.

function noisy(f) {
  return (...args) => {
    console.log("calling with", args);
    let result = f(...args);
    console.log("called with", args, ", returned", result);
    return result;
  };
}
noisy(Math.min)(3, 2, 1);
// → calling with [3, 2, 1]
// → called with [3, 2, 1] , returned 1

Kita bahkan bisa nulis fungsi yang nyediain jenis alur kontrol baru.

function unless(test, then) {
  if (!test) then();
}
 
repeat(3, n => {
  unless(n % 2 == 1, () => {
    console.log(n, "is even");
  });
});
// → 0 is even
// → 2 is even

Ada method array bawaan, forEach, yang nyediain sesuatu kaya loop for/of sebagai higher-order function.

["A", "B"].forEach(l => console.log(l));
// → A
// → B

Dataset Script

Salah satu area di mana higher-order functions bersinar adalah pemrosesan data. Buat ngeproses data, kita bakal butuh data contoh beneran. Bab ini bakal pakai dataset tentang script, sistem tulisan kaya Latin, Kiril, atau Arab.

Ingat Unicode, sistem yang ngasih angka ke setiap karakter di bahasa tulis, dari Bab 1? Kebanyakan karakter ini terkait sama script tertentu. Standarnya berisi 140 script berbeda, 81 di antaranya masih dipakai sekarang dan 59 bersejarah.

Walaupun saya cuma bisa baca lancar karakter Latin, saya menghargai fakta bahwa orang-orang nulis teks di setidaknya 80 sistem tulisan lain, banyak di antaranya bahkan gak bakal saya kenali. Misalnya, ini contoh tulisan tangan Tamil:

Satu baris syair dalam tulisan tangan Tamil. Karakternya relatif sederhana, dan terpisah rapi, tapi sepenuhnya berbeda dari Latin.

Dataset contohnya berisi beberapa potongan informasi tentang 140 script yang didefinisikan di Unicode. Dia tersedia di sandbox coding buat bab ini sebagai binding SCRIPTS. Bindingnya berisi array objek, yang masing-masing menggambarkan sebuah script.

{
  name: "Coptic",
  ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
  direction: "ltr",
  year: -200,
  living: false,
  link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}

Objek kaya gitu ngasih tahu kita nama script-nya, rentang Unicode yang ditetapkan ke dia, arah penulisannya, waktu asal (perkiraan), apakah masih dipakai, dan link ke informasi lebih lanjut. Arahnya bisa "ltr" buat kiri ke kanan, "rtl" buat kanan ke kiri (cara teks Arab dan Ibrani ditulis), atau "ttb" buat atas ke bawah (kaya tulisan Mongolia).

Properti ranges berisi array rentang karakter Unicode, yang masing-masing adalah array dua elemen yang berisi batas bawah dan batas atas. Kode karakter apa pun di dalam rentang ini ditetapkan ke script itu. Batas bawahnya inklusif (kode 994 adalah karakter Koptik) dan batas atasnya non-inklusif (kode 1008 bukan).

Memfilter Array

Kalau kita mau nemuin script di dataset yang masih dipakai, fungsi berikut mungkin membantu. Dia nyaring keluar elemen di array yang gak lolos tes.

function filter(array, test) {
  let passed = [];
  for (let element of array) {
    if (test(element)) {
      passed.push(element);
    }
  }
  return passed;
}
 
console.log(filter(SCRIPTS, script => script.living));
// → [{name: "Adlam", …}, …]

Fungsinya pakai argumen bernama test, sebuah nilai fungsi, buat ngisi “celah” di perhitungannya, proses mutusin elemen mana yang dikumpulin.

Perhatiin gimana fungsi filter, bukannya ngehapus elemen dari array yang ada, ngebangun array baru cuma dengan elemen yang lolos tes. Fungsi ini murni. Dia gak memodifikasi array yang dikasih ke dia.

Kaya forEach, filter adalah method array standar. Contohnya mendefinisikan fungsinya cuma buat nunjukin apa yang dia lakuin secara internal. Mulai sekarang, kita bakal pakainya kaya gini:

console.log(SCRIPTS.filter(s => s.direction == "ttb"));
// → [{name: "Mongolian", …}, …]

Transformasi dengan map

Misalnya kita punya array objek yang mewakili script, dihasilin dengan memfilter array SCRIPTS entah gimana. Kita mau array nama sebagai gantinya, yang lebih gampang diperiksa.

Method map mentransformasi array dengan nerapin fungsi ke semua elemennya dan ngebangun array baru dari nilai-nilai yang dikembaliin. Array barunya bakal punya panjang yang sama dengan array input, tapi isinya udah dipetakan (mapped) ke bentuk baru oleh fungsinya.

function map(array, transform) {
  let mapped = [];
  for (let element of array) {
    mapped.push(transform(element));
  }
  return mapped;
}
 
let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl");
console.log(map(rtlScripts, s => s.name));
// → ["Adlam", "Arabic", "Imperial Aramaic", …]

Kaya forEach dan filter, map adalah method array standar.

Meringkas dengan reduce

Hal umum lain yang dilakuin sama array adalah ngitung satu nilai dari mereka. Contoh berulang kita, ngejumlahin kumpulan angka, adalah instance dari ini. Contoh lain adalah nemuin script dengan karakter terbanyak.

Operasi tingkat tinggi yang mewakili pola ini disebut reduce (kadang juga disebut fold). Dia ngebangun nilai dengan berulang kali ngambil satu elemen dari array dan ngegabunginnya dengan nilai saat ini. Waktu ngejumlahin angka, kamu mulai dengan angka nol dan, buat setiap elemen, nambahin itu ke jumlahnya.

Parameter ke reduce adalah, selain array-nya, fungsi penggabung dan nilai awal. Fungsi ini sedikit kurang lugas dibanding filter dan map, jadi perhatiin baik-baik:

function reduce(array, combine, start) {
  let current = start;
  for (let element of array) {
    current = combine(current, element);
  }
  return current;
}
 
console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// → 10

Method array standar reduce, yang tentu aja sesuai dengan fungsi ini, punya kemudahan tambahan. Kalau array-mu berisi minimal satu elemen, kamu boleh ngilangin argumen start. Method-nya bakal ngambil elemen pertama array sebagai nilai awalnya dan mulai ngereduksi di elemen kedua.

console.log([1, 2, 3, 4].reduce((a, b) => a + b));
// → 10

Buat pakai reduce (dua kali) buat nemuin script dengan karakter terbanyak, kita bisa nulis sesuatu kaya gini:

function characterCount(script) {
  return script.ranges.reduce((count, [from, to]) => {
    return count + (to - from);
  }, 0);
}
 
console.log(SCRIPTS.reduce((a, b) => {
  return characterCount(a) < characterCount(b) ? b : a;
}));
// → {name: "Han", …}

Fungsi characterCount ngereduksi rentang yang ditetapkan ke sebuah script dengan ngejumlahin ukurannya. Perhatiin penggunaan destructuring di daftar parameter fungsi reducer-nya. Pemanggilan kedua ke reduce lalu pakai ini buat nemuin script terbesar dengan berulang kali ngebandingin dua script dan ngembaliin yang lebih besar.

Script Han punya lebih dari 89.000 karakter yang ditetapkan ke dia di standar Unicode, bikinnya jadi sistem tulisan terbesar sejauh ini di dataset. Han adalah script yang kadang dipakai buat teks Cina, Jepang, dan Korea. Bahasa-bahasa itu berbagi banyak karakter, walaupun mereka cenderung nulisnya beda. Unicode Consortium (yang berbasis di AS) mutusin buat memperlakukan mereka sebagai satu sistem tulisan buat ngehemat kode karakter. Ini disebut Han unification dan masih bikin beberapa orang sangat marah.

Composability

Bayangin gimana kita bakal nulis contoh sebelumnya (nemuin script terbesar) tanpa higher-order functions. Kodenya gak jauh lebih buruk.

let biggest = null;
for (let script of SCRIPTS) {
  if (biggest == null ||
      characterCount(biggest) < characterCount(script)) {
    biggest = script;
  }
}
console.log(biggest);
// → {name: "Han", …}

Ada beberapa binding lagi, dan programnya empat baris lebih panjang, tapi masih sangat bisa dibaca.

Abstraksi yang disediain fungsi-fungsi ini beneran bersinar waktu kamu perlu nyusun (compose) operasi. Sebagai contoh, kita tulis kode yang nemuin rata-rata tahun asal buat script yang hidup dan mati di dataset.

function average(array) {
  return array.reduce((a, b) => a + b) / array.length;
}
 
console.log(Math.round(average(
  SCRIPTS.filter(s => s.living).map(s => s.year))));
// → 1165
console.log(Math.round(average(
  SCRIPTS.filter(s => !s.living).map(s => s.year))));
// → 204

Kaya yang kamu lihat, script mati di Unicode, rata-rata, lebih tua dari yang hidup. Ini bukan statistik yang sangat bermakna atau mengejutkan. Tapi saya harap kamu setuju bahwa kode yang dipakai buat ngitungnya gak susah dibaca. Kamu bisa ngelihatnya sebagai pipeline: kita mulai dengan semua script, filter yang hidup (atau mati), ambil tahunnya, rata-ratakan, dan bulatkan hasilnya.

Kamu pasti juga bisa nulis perhitungan ini sebagai satu loop besar.

let total = 0, count = 0;
for (let script of SCRIPTS) {
  if (script.living) {
    total += script.year;
    count += 1;
  }
}
console.log(Math.round(total / count));
// → 1165

Tapi, lebih susah ngelihat apa yang dihitung dan gimana. Dan karena hasil antara gak diwakili sebagai nilai yang koheren, bakal jauh lebih banyak kerjaan buat ngekstrak sesuatu kaya average jadi fungsi terpisah.

Dalam hal apa yang sebenernya dilakuin komputer, dua pendekatan ini juga cukup beda. Yang pertama bakal ngebangun array baru waktu ngejalanin filter dan map, sedangkan yang kedua cuma ngitung beberapa angka, ngelakuin lebih sedikit kerjaan. Kamu biasanya mampu pakai pendekatan yang bisa dibaca, tapi kalau kamu ngeproses array yang sangat besar dan ngelakuinnya berkali-kali, gaya yang kurang abstrak mungkin layak buat kecepatan ekstranya.

String dan Kode Karakter

Salah satu penggunaan menarik dari dataset ini adalah nyari tahu script apa yang dipakai sepotong teks. Kita bahas program yang ngelakuin ini.

Ingat bahwa setiap script punya array rentang kode karakter yang terkait dengannya. Dikasih kode karakter, kita bisa pakai fungsi kaya gini buat nemuin script yang sesuai (kalau ada):

function characterScript(code) {
  for (let script of SCRIPTS) {
    if (script.ranges.some(([from, to]) => {
      return code >= from && code < to;
    })) {
      return script;
    }
  }
  return null;
}
 
console.log(characterScript(121));
// → {name: "Latin", …}

Method some adalah higher-order function lain. Dia ngambil fungsi tes dan ngasih tahu kamu apakah fungsi itu ngembaliin true buat salah satu elemen di array.

Tapi gimana kita dapet kode karakter di sebuah string?

Di Bab 1 saya nyebutin bahwa string JavaScript dikodekan sebagai deretan angka 16-bit. Ini disebut code unit. Kode karakter Unicode awalnya dimaksudkan buat muat di dalam satu unit kaya gitu (yang ngasih kamu sedikit lebih dari 65.000 karakter). Waktu jadi jelas itu gak bakal cukup, banyak orang keberatan sama kebutuhan pakai lebih banyak memori per karakter. Buat ngatasin kekhawatiran ini, UTF-16, format yang juga dipakai string JavaScript, diciptain. Dia menggambarkan kebanyakan karakter umum pakai satu code unit 16-bit tapi pakai sepasang dua unit kaya gitu buat yang lain.

UTF-16 umumnya dianggap ide buruk sekarang. Dia keliatan hampir sengaja didesain buat ngundang kesalahan. Gampang nulis program yang pura-pura code unit dan karakter itu hal yang sama. Dan kalau bahasamu gak pakai karakter dua unit, itu bakal keliatan jalan baik-baik aja. Tapi begitu seseorang nyoba pakai program kaya gitu dengan beberapa karakter Cina yang kurang umum, dia rusak. Untungnya, dengan munculnya emoji, semua orang mulai pakai karakter dua unit, dan beban nanganin masalah kaya gitu terdistribusi lebih adil.

Sayangnya, operasi yang jelas pada string JavaScript, kaya dapet panjangnya lewat properti length dan ngakses isinya pakai kurung siku, cuma nanganin code unit.

// Dua karakter emoji, kuda dan sepatu
let horseShoe = "🐴👟";
console.log(horseShoe.length);
// → 4
console.log(horseShoe[0]);
// → (Setengah karakter yang gak valid)
console.log(horseShoe.charCodeAt(0));
// → 55357 (Kode dari setengah karakternya)
console.log(horseShoe.codePointAt(0));
// → 128052 (Kode sebenarnya buat emoji kuda)

Method charCodeAt JavaScript ngasih kamu code unit, bukan kode karakter penuh. Method codePointAt, yang ditambahin belakangan, emang ngasih karakter Unicode penuh, jadi kita bisa pakai itu buat dapet karakter dari string. Tapi argumen yang dikasih ke codePointAt masih indeks ke deretan code unit. Buat ngelewatin semua karakter di string, kita masih perlu nanganin pertanyaan apakah sebuah karakter makan satu atau dua code unit.

Di bab sebelumnya, saya nyebutin bahwa loop for/of juga bisa dipakai di string. Kaya codePointAt, jenis loop ini diperkenalkan di saat orang-orang sangat sadar sama masalah UTF-16. Waktu kamu pakainya buat ngeloop string, dia ngasih kamu karakter beneran, bukan code unit.

let roseDragon = "🌹🐉";
for (let char of roseDragon) {
  console.log(char);
}
// → 🌹
// → 🐉

Kalau kamu punya karakter (yang bakal berupa string satu atau dua code unit), kamu bisa pakai codePointAt(0) buat dapet kodenya.

Mengenali Teks

Kita punya fungsi characterScript dan cara buat ngeloop karakter dengan benar. Langkah berikutnya adalah ngitung karakter yang termasuk ke setiap script. Abstraksi penghitungan berikut bakal berguna di sana:

function countBy(items, groupName) {
  let counts = [];
  for (let item of items) {
    let name = groupName(item);
    let known = counts.find(c => c.name == name);
    if (!known) {
      counts.push({name, count: 1});
    } else {
      known.count++;
    }
  }
  return counts;
}
 
console.log(countBy([1, 2, 3, 4, 5], n => n > 2));
// → [{name: false, count: 2}, {name: true, count: 3}]

Fungsi countBy ngarepin koleksi (apa pun yang bisa kita loop dengan for/of) dan fungsi yang ngitung nama grup buat elemen tertentu. Dia ngembaliin array objek, yang masing-masing nyebutin grup dan ngasih tahu kamu jumlah elemen yang ditemuin di grup itu.

Dia pakai method array lain, find, yang ngelewatin elemen-elemen di array dan ngembaliin yang pertama di mana fungsinya ngembaliin true. Dia ngembaliin undefined waktu gak nemuin elemen kaya gitu.

Pakai countBy, kita bisa nulis fungsi yang ngasih tahu kita script mana yang dipakai di sepotong teks.

function textScripts(text) {
  let scripts = countBy(text, char => {
    let script = characterScript(char.codePointAt(0));
    return script ? script.name : "none";
  }).filter(({name}) => name != "none");
 
  let total = scripts.reduce((n, {count}) => n + count, 0);
  if (total == 0) return "No scripts found";
 
  return scripts.map(({name, count}) => {
    return `${Math.round(count * 100 / total)}% ${name}`;
  }).join(", ");
}
 
console.log(textScripts('英国的狗说"woof", 俄罗斯的狗说"тяв"'));
// → 61% Han, 22% Latin, 17% Cyrillic

Fungsinya pertama ngitung karakter berdasarkan nama, pakai characterScript buat ngasih mereka nama dan jatuh ke string "none" buat karakter yang bukan bagian dari script mana pun. Pemanggilan filter ngebuang entri buat "none" dari array hasil, karena kita gak tertarik sama karakter-karakter itu.

Biar bisa ngitung persentase, kita pertama butuh jumlah total karakter yang termasuk ke sebuah script, yang bisa kita hitung dengan reduce. Kalau kita gak nemuin karakter kaya gitu, fungsinya ngembaliin string tertentu. Kalau gak, dia mentransformasi entri penghitungan jadi string yang bisa dibaca dengan map lalu ngegabunginnya dengan join.

Ringkasan

Bisa ngasih nilai fungsi ke fungsi lain adalah aspek JavaScript yang sangat berguna. Dia ngebolehin kita nulis fungsi yang memodelkan perhitungan dengan “celah” di dalamnya. Kode yang manggil fungsi-fungsi ini bisa ngisi celahnya dengan nyediain nilai fungsi.

Array nyediain sejumlah method tingkat tinggi yang berguna. Kamu bisa pakai forEach buat ngeloop elemen di array. Method filter ngembaliin array baru yang cuma berisi elemen yang lolos fungsi predikat. Kamu bisa mentransformasi array dengan ngelewatin setiap elemen lewat fungsi pakai map. Kamu bisa pakai reduce buat ngegabungin semua elemen di array jadi satu nilai. Method some ngetes apakah ada elemen yang cocok dengan fungsi predikat tertentu, sedangkan find nemuin elemen pertama yang cocok dengan predikat.

Poin-poin pentingnya:

  1. Abstraksi = kosakata baru biar kamu bisa ngomongin masalah di level yang lebih tinggi, tanpa tenggelam di detail loop dan counter.
  2. Higher-order function = fungsi yang nerima fungsi sebagai argumen atau ngembaliin fungsi.
  3. Method array wajib hafal: forEach (loop), filter (saring), map (ubah tiap elemen), reduce (rangkum jadi satu nilai), some (ada yang cocok?), every (semua cocok?), find (yang pertama cocok).
  4. Method-method ini bisa dirangkai jadi pipeline yang gampang dibaca, walaupun sedikit lebih lambat dari satu loop besar.
  5. String JavaScript itu UTF-16: length dan [i] ngitung code unit, bukan karakter. Pakai for/of dan codePointAt buat karakter beneran (emoji, Han, dll).

Latihan

Flattening

Pakai method reduce dikombinasikan dengan method concat buat “meratakan” (flatten) array yang berisi array jadi satu array yang punya semua elemen dari array-array aslinya.

let arrays = [[1, 2, 3], [4, 5], [6]];
// Kodemu di sini.
// → [1, 2, 3, 4, 5, 6]

Solusi:

let arrays = [[1, 2, 3], [4, 5], [6]];
 
console.log(arrays.reduce((flat, current) => flat.concat(current), []));
// → [1, 2, 3, 4, 5, 6]

Loop Buatan Sendiri

Tulis higher-order function loop yang nyediain sesuatu kaya statement loop for. Dia harus ngambil sebuah nilai, fungsi tes, fungsi update, dan fungsi badan. Setiap iterasi, dia harus pertama ngejalanin fungsi tes ke nilai loop saat ini dan berhenti kalau itu ngembaliin false. Lalu dia harus manggil fungsi badan, ngasihnya nilai saat ini, dan terakhir manggil fungsi update buat bikin nilai baru dan mulai lagi dari awal.

Waktu mendefinisikan fungsinya, kamu bisa pakai loop biasa buat ngelakuin looping yang sebenarnya.

// Kodemu di sini.
 
loop(3, n => n > 0, n => n - 1, console.log);
// → 3
// → 2
// → 1

Solusi:

function loop(start, test, update, body) {
  for (let value = start; test(value); value = update(value)) {
    body(value);
  }
}
 
loop(3, n => n > 0, n => n - 1, console.log);
// → 3
// → 2
// → 1

Everything

Array juga punya method every yang analog dengan method some. Method ini ngembaliin true waktu fungsi yang dikasih ngembaliin true buat setiap elemen di array. Dalam satu sisi, some adalah versi operator || yang beraksi pada array, dan every kaya operator &&.

Implementasikan every sebagai fungsi yang ngambil array dan fungsi predikat sebagai parameter. Tulis dua versi, satu pakai loop dan satu pakai method some.

function every(array, test) {
  // Kodemu di sini.
}
 
console.log(every([1, 3, 5], n => n < 10));
// → true
console.log(every([2, 4, 16], n => n < 10));
// → false
console.log(every([], n => n < 10));
// → true

Petunjuk:

Kaya operator &&, method every bisa berhenti ngevaluasi elemen selanjutnya begitu dia nemuin satu yang gak cocok. Jadi versi berbasis loop bisa lompat keluar dari loop, dengan break atau return, begitu dia nemuin elemen di mana fungsi predikatnya ngembaliin false. Kalau loop-nya jalan sampai akhir tanpa nemuin elemen kaya gitu, kita tahu semua elemen cocok dan kita harus ngembaliin true.

Buat ngebangun every di atas some, kita bisa nerapin hukum De Morgan, yang nyatain bahwa a && b sama dengan !(!a || !b). Ini bisa digeneralisasi ke array, di mana semua elemen di array cocok kalau gak ada elemen di array yang gak cocok.

Solusi:

function every(array, predicate) {
  for (let element of array) {
    if (!predicate(element)) return false;
  }
  return true;
}
 
function every2(array, predicate) {
  return !array.some(element => !predicate(element));
}
 
console.log(every([1, 3, 5], n => n < 10));
// → true
console.log(every([2, 4, 16], n => n < 10));
// → false
console.log(every([], n => n < 10));
// → true

Arah Penulisan Dominan

Tulis fungsi yang ngitung arah penulisan dominan di sebuah string teks. Ingat bahwa setiap objek script punya properti direction yang bisa "ltr" (kiri ke kanan), "rtl" (kanan ke kiri), atau "ttb" (atas ke bawah).

Arah dominan adalah arah dari mayoritas karakter yang punya script terkait. Fungsi characterScript dan countBy yang didefinisikan sebelumnya di bab ini mungkin berguna di sini.

function dominantDirection(text) {
  // Kodemu di sini.
}
 
console.log(dominantDirection("Hello!"));
// → ltr
console.log(dominantDirection("Hey, مساء الخير"));
// → rtl

Petunjuk:

Solusimu mungkin keliatan sangat mirip setengah pertama contoh textScripts. Kamu lagi-lagi harus ngitung karakter berdasarkan kriteria yang didasarkan pada characterScript lalu nyaring keluar bagian hasil yang ngerujuk ke karakter yang gak menarik (tanpa script).

Nemuin arah dengan jumlah karakter tertinggi bisa dilakuin dengan reduce. Kalau gak jelas caranya, lihat contoh sebelumnya di bab ini, di mana reduce dipakai buat nemuin script dengan karakter terbanyak.

Solusi:

function dominantDirection(text) {
  let counted = countBy(text, char => {
    let script = characterScript(char.codePointAt(0));
    return script ? script.direction : "none";
  }).filter(({name}) => name != "none");
 
  if (counted.length == 0) return "ltr";
 
  return counted.reduce((a, b) => a.count > b.count ? a : b).name;
}
 
console.log(dominantDirection("Hello!"));
// → ltr
console.log(dominantDirection("Hey, مساء الخير"));
// → rtl