Bab 4 dari Eloquent JavaScript. Sebelumnya: 03 Fungsi. Berikutnya: 05 Fungsi Tingkat Tinggi.
Dua kali saya pernah ditanya, ‘Maaf, Tuan Babbage, kalau Anda masukin angka yang salah ke mesinnya, apakah jawaban yang benar bakal keluar?’ […] Saya gak bisa paham dengan benar jenis kebingungan gagasan macam apa yang bisa memunculkan pertanyaan kaya gitu.
— Charles Babbage, Passages from the Life of a Philosopher (1864)

Angka, Boolean, dan string adalah atom-atom yang jadi bahan pembangun struktur data. Tapi banyak jenis informasi yang butuh lebih dari satu atom. Objek ngebolehin kita ngelompokin nilai-nilai, termasuk objek lain, buat ngebangun struktur yang lebih kompleks.
Program yang udah kita bangun sejauh ini dibatasi oleh fakta bahwa mereka cuma beroperasi pada tipe data sederhana. Setelah belajar dasar-dasar struktur data di bab ini, kamu bakal cukup tahu buat mulai nulis program yang berguna.
Bab ini bakal ngerjain contoh pemrograman yang kurang lebih realistis, ngenalin konsep-konsep sesuai penerapannya ke masalah yang ada. Kode contohnya sering bakal dibangun di atas fungsi dan binding yang udah diperkenalkan sebelumnya di buku ini.
Manusia Tupai (Weresquirrel)
Sesekali, biasanya antara jam 8 dan 10 malam, Jacques nemuin dirinya berubah jadi hewan pengerat kecil berbulu dengan ekor lebat.
Di satu sisi, Jacques cukup senang dia gak kena lycanthropy klasik. Berubah jadi tupai emang bikin lebih sedikit masalah daripada berubah jadi serigala. Bukannya harus khawatir gak sengaja makan tetangga (itu bakal canggung), dia khawatir dimakan kucing tetangga. Setelah dua kali bangun di dahan tipis yang genting di puncak pohon ek, telanjang dan bingung, dia mulai ngunci pintu dan jendela kamarnya di malam hari dan naruh beberapa kenari di lantai biar dirinya sibuk.
Tapi Jacques lebih suka ngilangin kondisinya sepenuhnya. Kejadian transformasi yang gak teratur bikin dia curiga mungkin ada yang memicunya. Selama beberapa waktu, dia percaya itu cuma terjadi di hari-hari dia dekat pohon ek. Tapi, ngehindarin pohon ek gak nyelesaiin masalahnya.
Beralih ke pendekatan yang lebih ilmiah, Jacques mulai bikin catatan harian soal semua yang dia lakuin di hari tertentu dan apakah dia berubah bentuk. Dengan data ini dia berharap bisa mempersempit kondisi yang memicu transformasinya.
Hal pertama yang dia butuhin adalah struktur data buat nyimpen informasi ini.
Dataset
Buat kerja dengan sepotong data digital, kita harus dulu nemuin cara buat mewakilinya di memori mesin kita. Misalnya, kita mau mewakili kumpulan angka 2, 3, 5, 7, dan 11.
Kita bisa kreatif pakai string, lagipula, string bisa punya panjang berapa pun, jadi kita bisa naruh banyak data di dalamnya, dan pakai "2 3 5 7 11" sebagai representasi kita. Tapi ini canggung. Kita harus entah gimana ngekstrak digit-digitnya dan ngonversinya balik jadi angka buat ngaksesnya.
Untungnya, JavaScript nyediain tipe data khusus buat nyimpen deretan nilai. Namanya array dan ditulis sebagai daftar nilai di antara kurung siku, dipisah koma.
let listOfNumbers = [2, 3, 5, 7, 11];
console.log(listOfNumbers[2]);
// → 5
console.log(listOfNumbers[0]);
// → 2
console.log(listOfNumbers[2 - 1]);
// → 3Notasi buat ngambil elemen di dalam array juga pakai kurung siku. Sepasang kurung siku tepat setelah sebuah ekspresi, dengan ekspresi lain di dalamnya, bakal nyari elemen di ekspresi sebelah kiri yang sesuai dengan indeks yang dikasih ekspresi di dalam kurungnya.
Indeks pertama array adalah nol, bukan satu, jadi elemen pertama diambil dengan listOfNumbers[0]. Ngitung dari nol punya tradisi panjang di teknologi dan dalam beberapa hal sangat masuk akal, tapi butuh pembiasaan. Bayangin indeksnya sebagai jumlah item yang dilewatin, dihitung dari awal array.
Properti
Kita udah lihat beberapa ekspresi kaya myString.length (buat dapet panjang string) dan Math.max (fungsi maksimum) di bab-bab sebelumnya. Ekspresi-ekspresi ini ngakses properti dari suatu nilai. Di kasus pertama, kita ngakses properti length dari nilai di myString. Di kasus kedua, kita ngakses properti bernama max di objek Math (yang merupakan kumpulan konstanta dan fungsi terkait matematika).
Hampir semua nilai JavaScript punya properti. Pengecualiannya adalah null dan undefined. Kalau kamu nyoba ngakses properti di salah satu non-nilai ini, kamu dapet error:
null.length;
// → TypeError: null has no propertiesDua cara utama buat ngakses properti di JavaScript adalah pakai titik dan pakai kurung siku. value.x maupun value[x] sama-sama ngakses properti di value, tapi belum tentu properti yang sama. Bedanya ada di cara x ditafsirkan. Waktu pakai titik, kata setelah titik adalah nama literal propertinya. Waktu pakai kurung siku, ekspresi di antara kurungnya dievaluasi buat dapet nama propertinya. Sementara value.x ngambil properti value yang bernama “x”, value[x] ngambil nilai dari variabel bernama x dan pakai itu, dikonversi ke string, sebagai nama propertinya.
Kalau kamu tahu properti yang kamu minati bernama color, kamu tulis value.color. Kalau kamu mau ngekstrak properti yang namanya ada di nilai yang dipegang binding i, kamu tulis value[i]. Nama properti itu string. Bisa string apa pun, tapi notasi titik cuma jalan dengan nama yang keliatan kaya nama binding yang valid, dimulai dengan huruf atau underscore, dan cuma berisi huruf, angka, dan underscore. Kalau kamu mau ngakses properti bernama 2 atau John Doe, kamu harus pakai kurung siku: value[2] atau value["John Doe"].
Elemen-elemen di array disimpan sebagai properti array-nya, pakai angka sebagai nama properti. Karena kamu gak bisa pakai notasi titik dengan angka dan biasanya mau pakai binding yang megang indeksnya, kamu harus pakai notasi kurung siku buat ngambilnya.
Sama kaya string, array punya properti length yang ngasih tahu kita berapa banyak elemen yang dipunya array-nya.
Method
Nilai string maupun array berisi, selain properti length, sejumlah properti yang megang nilai fungsi.
let doh = "Doh";
console.log(typeof doh.toUpperCase);
// → function
console.log(doh.toUpperCase());
// → DOHSetiap string punya properti toUpperCase. Waktu dipanggil, dia bakal ngembaliin salinan string di mana semua hurufnya udah dikonversi jadi huruf besar. Ada juga toLowerCase, yang arahnya sebaliknya.
Menariknya, walaupun pemanggilan toUpperCase gak ngasih argumen apa pun, fungsinya entah gimana punya akses ke string "Doh", nilai yang propertinya kita panggil. Kamu bakal tahu cara kerjanya di Bab 6.
Properti yang berisi fungsi umumnya disebut method dari nilai yang memilikinya, kaya di “toUpperCase adalah method dari string”.
Contoh ini nunjukin dua method yang bisa kamu pakai buat memanipulasi array.
let sequence = [1, 2, 3];
sequence.push(4);
sequence.push(5);
console.log(sequence);
// → [1, 2, 3, 4, 5]
console.log(sequence.pop());
// → 5
console.log(sequence);
// → [1, 2, 3, 4]Method push nambahin nilai ke akhir array. Method pop ngelakuin kebalikannya, ngehapus nilai terakhir di array dan ngembaliinnya.
Nama-nama yang agak konyol ini adalah istilah tradisional buat operasi pada stack (tumpukan). Stack, di pemrograman, adalah struktur data yang ngebolehin kamu push (dorong) nilai ke dalamnya dan pop (ambil) mereka keluar lagi dengan urutan terbalik jadi yang ditambahin terakhir dihapus duluan. Stack umum di pemrograman, kamu mungkin ingat call stack fungsi dari bab sebelumnya, yang merupakan instance dari ide yang sama.
Objek
Balik ke manusia tupai. Sekumpulan entri catatan harian bisa diwakili sebagai array, tapi entrinya gak cuma terdiri dari angka atau string, setiap entri perlu nyimpen daftar aktivitas dan nilai Boolean yang nunjukin apakah Jacques berubah jadi tupai atau gak. Idealnya, kita mau ngelompokin ini jadi satu nilai lalu naruh nilai-nilai kelompok itu ke array entri catatan.
Nilai dengan tipe object adalah kumpulan properti yang sembarang. Salah satu cara bikin objek adalah pakai kurung kurawal sebagai ekspresi.
let day1 = {
squirrel: false,
events: ["work", "touched tree", "pizza", "running"]
};
console.log(day1.squirrel);
// → false
console.log(day1.wolf);
// → undefined
day1.wolf = false;
console.log(day1.wolf);
// → falseDi dalam kurung kurawal, kamu nulis daftar properti yang dipisah koma. Setiap properti punya nama diikuti titik dua dan nilai. Waktu objek ditulis di beberapa baris, ngasih indentasi kaya di contoh ini ngebantu keterbacaan. Properti yang namanya bukan nama binding valid atau angka valid harus dikutip:
let descriptions = {
work: "Went to work",
"touched tree": "Touched a tree"
};Ini berarti kurung kurawal punya dua makna di JavaScript. Di awal statement, mereka memulai block statement. Di posisi lain mana pun, mereka menggambarkan objek. Untungnya, jarang berguna buat mulai statement dengan objek dalam kurung kurawal, jadi ambiguitas antara keduanya gak terlalu jadi masalah. Satu kasus di mana ini muncul adalah waktu kamu mau ngembaliin objek dari arrow function singkat, kamu gak bisa nulis n => {prop: n} karena kurung kurawalnya bakal ditafsirkan sebagai badan fungsi. Sebagai gantinya, kamu harus naruh sepasang tanda kurung di sekitar objeknya biar jelas itu ekspresi.
❌ salah, kurung kurawal dianggap badan fungsi, bukan objek
let makeObj = n => {prop: n};
console.log(makeObj(1));
// → undefined✅ benar, dibungkus tanda kurung biar jelas itu ekspresi objek
let makeObj = n => ({prop: n});
console.log(makeObj(1));
// → {prop: 1}Baca properti yang gak ada bakal ngasih kamu nilai undefined.
Bisa aja nge-assign nilai ke ekspresi properti pakai operator =. Ini bakal ngegantiin nilai propertinya kalau udah ada atau bikin properti baru di objeknya kalau belum ada.
Balik sebentar ke model tentakel binding kita, binding properti itu mirip. Mereka menggenggam nilai, tapi binding dan properti lain mungkin lagi megang nilai yang sama. Kamu bisa ngebayangin objek sebagai gurita dengan berapa pun tentakel, yang masing-masing punya nama tertulis di atasnya.
Operator delete motong tentakel dari gurita kaya gitu. Dia operator unary yang, waktu diterapin ke properti objek, bakal ngehapus properti bernama itu dari objeknya. Ini bukan hal yang umum dilakuin, tapi bisa.
let anObject = {left: 1, right: 2};
console.log(anObject.left);
// → 1
delete anObject.left;
console.log(anObject.left);
// → undefined
console.log("left" in anObject);
// → false
console.log("right" in anObject);
// → trueOperator binary in, waktu diterapin ke string dan objek, ngasih tahu kamu apakah objek itu punya properti dengan nama itu. Perbedaan antara nyetel properti ke undefined dan beneran ngehapusnya adalah di kasus pertama, objeknya masih punya propertinya (cuma nilainya gak terlalu menarik), sedangkan di kasus kedua, propertinya udah gak ada dan in bakal ngembaliin false.
Buat tahu properti apa aja yang dipunya objek, kamu bisa pakai fungsi Object.keys. Kasih fungsinya sebuah objek dan dia bakal ngembaliin array string, nama-nama properti objeknya:
console.log(Object.keys({x: 0, y: 0, z: 2}));
// → ["x", "y", "z"]Ada fungsi Object.assign yang nyalin semua properti dari satu objek ke objek lain:
let objectA = {a: 1, b: 2};
Object.assign(objectA, {b: 3, c: 4});
console.log(objectA);
// → {a: 1, b: 3, c: 4}Jadi array cuma semacam objek yang dikhususkan buat nyimpen deretan sesuatu. Kalau kamu evaluasi typeof [], hasilnya "object". Kamu bisa ngebayangin array sebagai gurita panjang dan pipih dengan semua tentakelnya berjajar rapi, dilabeli angka.
Jacques bakal mewakili jurnal yang dia simpan sebagai array objek:
let journal = [
{events: ["work", "touched tree", "pizza",
"running", "television"],
squirrel: false},
{events: ["work", "ice cream", "cauliflower",
"lasagna", "touched tree", "brushed teeth"],
squirrel: false},
{events: ["weekend", "cycling", "break", "peanuts",
"beer"],
squirrel: true},
/* Dan seterusnya... */
];Mutability
Kita bakal segera nyampe ke pemrograman beneran, tapi pertama, ada satu lagi teori yang harus dipahami.
Kita lihat nilai objek bisa dimodifikasi. Tipe-tipe nilai yang dibahas di bab-bab sebelumnya, kaya angka, string, dan Boolean, semuanya immutable, gak mungkin ngubah nilai dari tipe-tipe itu. Kamu bisa ngegabungin mereka dan nurunin nilai baru dari mereka, tapi waktu kamu ngambil nilai string tertentu, nilai itu bakal selalu tetap sama. Teks di dalamnya gak bisa diubah. Kalau kamu punya string yang berisi "cat", gak mungkin buat kode lain ngubah satu karakter di stringmu biar jadi "rat".
Objek bekerja beda. Kamu bisa ngubah propertinya, bikin satu nilai objek punya isi yang berbeda di waktu yang berbeda.
Waktu kita punya dua angka, 120 dan 120, kita bisa nganggap mereka persis angka yang sama, terlepas dari apakah mereka nunjuk ke bit fisik yang sama. Dengan objek, ada perbedaan antara punya dua referensi ke objek yang sama dan punya dua objek berbeda yang berisi properti yang sama. Lihat kode berikut:
let object1 = {value: 10};
let object2 = object1;
let object3 = {value: 10};
console.log(object1 == object2);
// → true
console.log(object1 == object3);
// → false
object1.value = 15;
console.log(object2.value);
// → 15
console.log(object3.value);
// → 10Binding object1 dan object2 menggenggam objek yang sama, makanya ngubah object1 juga ngubah nilai object2. Mereka dibilang punya identitas yang sama. Binding object3 nunjuk ke objek berbeda, yang awalnya berisi properti yang sama dengan object1 tapi hidup terpisah.
Binding juga bisa berubah atau konstan, tapi ini terpisah dari cara nilainya berperilaku. Walaupun nilai angka gak berubah, kamu bisa pakai binding let buat ngelacak angka yang berubah dengan ngubah nilai yang ditunjuk bindingnya. Sama juga, walaupun binding const ke objek sendiri gak bisa diubah dan bakal terus nunjuk ke objek yang sama, isi objek itu mungkin berubah.
const score = {visitors: 0, home: 0};
// Ini boleh
score.visitors = 1;
// Ini gak boleh
score = {visitors: 1, home: 1};Waktu kamu ngebandingin objek pakai operator == JavaScript, dia ngebandingin berdasarkan identitas: dia bakal ngehasilin true cuma kalau kedua objeknya persis nilai yang sama. Ngebandingin objek yang berbeda bakal ngembaliin false, bahkan kalau propertinya identik. Gak ada operasi perbandingan “dalam” (deep) bawaan di JavaScript yang ngebandingin objek berdasarkan isinya, tapi bisa aja nulisnya sendiri (yang merupakan salah satu latihan di akhir bab ini).
Catatan si Lycanthrope
Jacques nyalain interpreter JavaScript-nya dan nyiapin environment yang dia butuhin buat nyimpen jurnalnya:
let journal = [];
function addEntry(events, squirrel) {
journal.push({events, squirrel});
}Perhatiin objek yang ditambahin ke jurnal keliatan agak aneh. Bukannya mendeklarasikan properti kaya events: events, dia cuma ngasih nama properti: events. Ini singkatan yang artinya sama, kalau nama properti di notasi kurung kurawal gak diikuti nilai, nilainya diambil dari binding dengan nama yang sama.
Setiap malam jam 10, atau kadang pagi berikutnya, setelah turun dari rak paling atas lemari bukunya, Jacques nyatet harinya:
addEntry(["work", "touched tree", "pizza", "running",
"television"], false);
addEntry(["work", "ice cream", "cauliflower", "lasagna",
"touched tree", "brushed teeth"], false);
addEntry(["weekend", "cycling", "break", "peanuts",
"beer"], true);Begitu dia punya cukup titik data, dia berniat pakai statistik buat nemuin mana dari kejadian-kejadian ini yang mungkin berhubungan sama pentupaian.
Korelasi adalah ukuran ketergantungan antara variabel statistik. Variabel statistik gak persis sama dengan variabel pemrograman. Di statistik kamu biasanya punya sekumpulan pengukuran, dan setiap variabel diukur buat setiap pengukuran. Korelasi antar variabel biasanya dinyatain sebagai nilai yang berkisar dari -1 sampai 1. Korelasi nol berarti variabelnya gak berhubungan. Korelasi 1 nunjukin keduanya berhubungan sempurna, kalau kamu tahu yang satu, kamu juga tahu yang lain. Negatif 1 juga berarti variabelnya berhubungan sempurna tapi berlawanan, waktu yang satu true, yang lain false.
Buat ngitung ukuran korelasi antara dua variabel Boolean, kita bisa pakai koefisien phi (ϕ). Ini rumus yang inputnya tabel frekuensi yang berisi berapa kali kombinasi-kombinasi berbeda dari variabelnya diamati. Output rumusnya adalah angka antara -1 dan 1 yang menggambarkan korelasinya.
Kita bisa ngambil kejadian makan pizza dan naruhnya di tabel frekuensi kaya gini, di mana setiap angka nunjukin berapa kali kombinasi itu terjadi di pengukuran kita.
Kalau kita sebut tabel itu n, kita bisa ngitung ϕ pakai rumus berikut:
n₁₁n₀₀ − n₁₀n₀₁
ϕ = ─────────────────────
√(n₁• n₀• n•₁ n•₀)(Kalau di titik ini kamu naruh bukunya buat fokus ke kilas balik mengerikan pelajaran matematika kelas 10, tahan dulu! Saya gak berniat nyiksa kamu dengan halaman-halaman notasi samar tanpa akhir, cuma satu rumus ini buat sekarang. Dan bahkan dengan yang satu ini, yang kita lakuin cuma ngubahnya jadi JavaScript.)
Notasi n₀₁ nunjukin jumlah pengukuran di mana variabel pertama (ketupaian) false (0) dan variabel kedua (pizza) true (1). Di tabel pizza, n₀₁ adalah 9.
Nilai n₁• ngerujuk ke jumlah semua pengukuran di mana variabel pertama true, yaitu 5 di tabel contoh. Sama juga, n•₀ ngerujuk ke jumlah pengukuran di mana variabel kedua false.
Jadi buat tabel pizza, bagian di atas garis pembagi (pembilang) adalah 1×76−4×9 = 40, dan bagian di bawahnya (penyebut) adalah akar kuadrat dari 5×85×10×80, atau √340.000. Hasilnya ϕ ≈ 0,069, yang kecil banget. Makan pizza keliatannya gak berpengaruh ke transformasinya.
Ngitung Korelasi
Kita bisa mewakili tabel dua kali dua di JavaScript dengan array empat elemen ([76, 9, 4, 1]). Kita juga bisa pakai representasi lain, kaya array yang berisi dua array dua elemen ([[76, 9], [4, 1]]) atau objek dengan nama properti kaya "11" dan "01", tapi array pipih itu sederhana dan bikin ekspresi yang ngakses tabelnya pendek dan enak. Kita bakal nafsirin indeks ke array sebagai angka biner dua bit, di mana digit paling kiri (paling signifikan) ngerujuk ke variabel tupai dan digit paling kanan (paling gak signifikan) ngerujuk ke variabel kejadian. Misalnya, angka biner 10 ngerujuk ke kasus di mana Jacques emang berubah jadi tupai, tapi kejadiannya (misalnya, “pizza”) gak terjadi. Ini terjadi empat kali. Dan karena biner 10 adalah 2 di notasi desimal, kita bakal nyimpen angka ini di indeks 2 dari array.
Ini fungsi yang ngitung koefisien ϕ dari array kaya gitu:
function phi(table) {
return (table[3] * table[0] - table[2] * table[1]) /
Math.sqrt((table[2] + table[3]) *
(table[0] + table[1]) *
(table[1] + table[3]) *
(table[0] + table[2]));
}
console.log(phi([76, 9, 4, 1]));
// → 0.068599434Ini terjemahan langsung rumus ϕ ke JavaScript. Math.sqrt adalah fungsi akar kuadrat, yang disediain objek Math di environment JavaScript standar. Kita harus nambahin dua field dari tabel buat dapet field kaya n₁• karena jumlah baris atau kolom gak disimpan langsung di struktur data kita.
Jacques nyimpen jurnalnya selama tiga bulan. Dataset hasilnya tersedia di sandbox coding buat bab ini, di mana dia disimpan di binding JOURNAL, dan di file yang bisa diunduh.
Buat ngekstrak tabel dua kali dua buat kejadian tertentu dari jurnal, kita harus ngeloop semua entri dan ngitung berapa kali kejadiannya terjadi dalam kaitannya dengan transformasi tupai:
function tableFor(event, journal) {
let table = [0, 0, 0, 0];
for (let i = 0; i < journal.length; i++) {
let entry = journal[i], index = 0;
if (entry.events.includes(event)) index += 1;
if (entry.squirrel) index += 2;
table[index] += 1;
}
return table;
}
console.log(tableFor("pizza", JOURNAL));
// → [76, 9, 4, 1]Array punya method includes yang ngecek apakah nilai yang dikasih ada di array. Fungsinya pakai itu buat nentuin apakah nama kejadian yang dia minati adalah bagian dari daftar kejadian buat hari tertentu.
Badan loop di tableFor nyari tahu kotak mana di tabel tempat setiap entri jurnal jatuh dengan ngecek apakah entrinya berisi kejadian spesifik yang dia minati dan apakah kejadiannya terjadi bareng insiden tupai. Loop-nya lalu nambahin satu ke kotak yang benar di tabel.
Sekarang kita punya alat yang kita butuhin buat ngitung korelasi individual. Satu-satunya langkah tersisa adalah nemuin korelasi buat setiap jenis kejadian yang dicatat dan lihat apakah ada yang menonjol.
Loop Array
Di fungsi tableFor, ada loop kaya gini:
for (let i = 0; i < JOURNAL.length; i++) {
let entry = JOURNAL[i];
// Lakuin sesuatu dengan entry
}Loop jenis ini umum di JavaScript klasik, ngelewatin array satu elemen per satu adalah sesuatu yang sering muncul, dan buat ngelakuinnya kamu ngejalanin counter sepanjang array dan ngambil setiap elemen bergantian.
Ada cara yang lebih sederhana buat nulis loop kaya gitu di JavaScript modern:
for (let entry of JOURNAL) {
console.log(`${entry.events.length} events.`);
}Waktu loop for pakai kata of setelah definisi variabelnya, dia bakal ngeloop elemen-elemen dari nilai yang dikasih setelah of. Ini jalan bukan cuma buat array tapi juga buat string dan beberapa struktur data lain. Kita bakal ngebahas gimana cara kerjanya di Bab 6.
Analisis Akhir
Kita perlu ngitung korelasi buat setiap jenis kejadian yang muncul di dataset. Buat itu, kita pertama perlu nemuin setiap jenis kejadian.
function journalEvents(journal) {
let events = [];
for (let entry of journal) {
for (let event of entry.events) {
if (!events.includes(event)) {
events.push(event);
}
}
}
return events;
}
console.log(journalEvents(JOURNAL));
// → ["carrot", "exercise", "weekend", "bread", …]Dengan nambahin nama kejadian apa pun yang belum ada di dalamnya ke array events, fungsinya ngumpulin setiap jenis kejadian.
Pakai fungsi itu, kita bisa lihat semua korelasinya:
for (let event of journalEvents(JOURNAL)) {
console.log(event + ":", phi(tableFor(event, JOURNAL)));
}
// → carrot: 0.0140970969
// → exercise: 0.0685994341
// → weekend: 0.1371988681
// → bread: -0.0757554019
// → pudding: -0.0648203724
// Dan seterusnya...Kebanyakan korelasi keliatannya dekat nol. Makan wortel, roti, atau puding ternyata gak memicu lycanthropy tupai. Transformasinya emang keliatan agak lebih sering terjadi di akhir pekan. Kita filter hasilnya buat cuma nampilin korelasi yang lebih besar dari 0,1 atau lebih kecil dari -0,1:
for (let event of journalEvents(JOURNAL)) {
let correlation = phi(tableFor(event, JOURNAL));
if (correlation > 0.1 || correlation < -0.1) {
console.log(event + ":", correlation);
}
}
// → weekend: 0.1371988681
// → brushed teeth: -0.3805211953
// → candy: 0.1296407447
// → work: -0.1371988681
// → spaghetti: 0.2425356250
// → reading: 0.1106828054
// → peanuts: 0.5902679812Aha! Ada dua faktor dengan korelasi yang jelas lebih kuat dari yang lain. Makan kacang punya efek positif yang kuat ke peluang berubah jadi tupai, sedangkan gosok gigi punya efek negatif yang signifikan.
Menarik. Kita coba sesuatu.
for (let entry of JOURNAL) {
if (entry.events.includes("peanuts") &&
!entry.events.includes("brushed teeth")) {
entry.events.push("peanut teeth");
}
}
console.log(phi(tableFor("peanut teeth", JOURNAL)));
// → 1Itu hasil yang kuat. Fenomenanya terjadi persis waktu Jacques makan kacang dan gak gosok gigi. Kalau aja dia gak sejorok itu soal kebersihan gigi, dia bahkan gak bakal pernah nyadar penyakitnya.
Setelah tahu ini, Jacques berhenti makan kacang sama sekali dan nemuin transformasinya berhenti.
Tapi cuma butuh beberapa bulan buat dia nyadar ada yang hilang dari cara hidup yang sepenuhnya manusiawi ini. Tanpa petualangan liarnya, Jacques hampir gak ngerasa hidup sama sekali. Dia mutusin lebih baik jadi hewan liar penuh waktu. Setelah ngebangun rumah pohon kecil yang indah di hutan dan ngelengkapinnya dengan dispenser selai kacang dan persediaan selai kacang buat sepuluh tahun, dia berubah bentuk buat terakhir kalinya, dan menjalani hidup tupai yang singkat dan energik.
Array-ologi Lanjutan
Sebelum nyelesaiin bab ini, saya mau ngenalin beberapa konsep terkait objek lagi. Saya mulai dengan beberapa method array yang umumnya berguna.
Kita udah lihat push dan pop, yang nambahin dan ngehapus elemen di akhir array, sebelumnya di bab ini. Method yang sesuai buat nambahin dan ngehapus sesuatu di awal array disebut unshift dan shift.
let todoList = [];
function remember(task) {
todoList.push(task);
}
function getTask() {
return todoList.shift();
}
function rememberUrgently(task) {
todoList.unshift(task);
}Program ini ngatur antrean tugas. Kamu nambahin tugas ke akhir antrean dengan manggil remember("groceries"), dan waktu kamu siap ngelakuin sesuatu, kamu manggil getTask() buat dapet (dan ngehapus) item terdepan dari antrean. Fungsi rememberUrgently juga nambahin tugas tapi nambahinnya ke depan bukannya ke belakang antrean.
Buat nyari nilai tertentu, array nyediain method indexOf. Method-nya nyari di array dari awal sampai akhir dan ngembaliin indeks tempat nilai yang diminta ditemuin, atau -1 kalau gak ketemu. Buat nyari dari akhir bukannya dari awal, ada method serupa bernama lastIndexOf:
console.log([1, 2, 3, 2, 1].indexOf(2));
// → 1
console.log([1, 2, 3, 2, 1].lastIndexOf(2));
// → 3indexOf maupun lastIndexOf ngambil argumen kedua opsional yang nunjukin dari mana mulai nyari.
Method array fundamental lainnya adalah slice, yang ngambil indeks awal dan akhir dan ngembaliin array yang cuma punya elemen di antara mereka. Indeks awalnya inklusif dan indeks akhirnya eksklusif.
console.log([0, 1, 2, 3, 4].slice(2, 4));
// → [2, 3]
console.log([0, 1, 2, 3, 4].slice(2));
// → [2, 3, 4]Waktu indeks akhirnya gak dikasih, slice bakal ngambil semua elemen setelah indeks awal. Kamu juga bisa ngilangin indeks awalnya buat nyalin seluruh array.
Method concat bisa dipakai buat nyambungin array jadi array baru, mirip yang dilakuin operator + buat string.
Contoh berikut nunjukin concat dan slice beraksi. Dia ngambil array dan indeks dan ngembaliin array baru yang merupakan salinan array aslinya dengan elemen di indeks yang dikasih dihapus:
function remove(array, index) {
return array.slice(0, index)
.concat(array.slice(index + 1));
}
console.log(remove(["a", "b", "c", "d", "e"], 2));
// → ["a", "b", "d", "e"]Kalau kamu ngasih concat argumen yang bukan array, nilai itu bakal ditambahin ke array baru seolah dia array satu elemen.
String dan Propertinya
Kita bisa baca properti kaya length dan toUpperCase dari nilai string. Tapi kalau kita nyoba nambahin properti baru, dia gak nempel.
let kim = "Kim";
kim.age = 88;
console.log(kim.age);
// → undefinedNilai bertipe string, number, dan Boolean bukan objek, dan walaupun bahasanya gak protes kalau kamu nyoba nyetel properti baru di mereka, dia sebenernya gak nyimpen properti itu. Kaya yang disebutin sebelumnya, nilai kaya gitu immutable dan gak bisa diubah.
Tapi tipe-tipe ini punya properti bawaan. Setiap nilai string punya sejumlah method. Beberapa yang sangat berguna adalah slice dan indexOf, yang mirip method array dengan nama yang sama:
console.log("coconuts".slice(4, 7));
// → nut
console.log("coconut".indexOf("u"));
// → 5Satu perbedaannya adalah indexOf string bisa nyari string yang berisi lebih dari satu karakter, sedangkan method array yang sesuai cuma nyari satu elemen:
console.log("one two three".indexOf("ee"));
// → 11Method trim ngehapus whitespace (spasi, newline, tab, dan karakter serupa) dari awal dan akhir string:
console.log(" okay \n ".trim());
// → okayFungsi zeroPad dari bab sebelumnya juga ada sebagai method. Namanya padStart dan ngambil panjang yang diinginkan dan karakter padding sebagai argumen:
console.log(String(6).padStart(3, "0"));
// → 006Kamu bisa mecah string di setiap kemunculan string lain pakai split dan ngegabunginnya lagi pakai join:
let sentence = "Secretarybirds specialize in stomping";
let words = sentence.split(" ");
console.log(words);
// → ["Secretarybirds", "specialize", "in", "stomping"]
console.log(words.join(". "));
// → Secretarybirds. specialize. in. stompingString bisa diulang pakai method repeat, yang bikin string baru yang berisi beberapa salinan string aslinya, dilem jadi satu:
console.log("LA".repeat(3));
// → LALALAKita udah lihat properti length dari tipe string. Ngakses karakter individual di string keliatan kaya ngakses elemen array (dengan komplikasi yang bakal kita bahas di Bab 5).
let string = "abc";
console.log(string.length);
// → 3
console.log(string[1]);
// → bRest Parameter
Bisa berguna buat fungsi nerima berapa pun argumen. Misalnya, Math.max ngitung maksimum dari semua argumen yang dikasih ke dia. Buat nulis fungsi kaya gitu, kamu naruh tiga titik sebelum parameter terakhir fungsinya, kaya gini:
function max(...numbers) {
let result = -Infinity;
for (let number of numbers) {
if (number > result) result = number;
}
return result;
}
console.log(max(4, 1, 9, -2));
// → 9Waktu fungsi kaya gitu dipanggil, rest parameter-nya terikat ke array yang berisi semua argumen selanjutnya. Kalau ada parameter lain sebelumnya, nilai mereka bukan bagian dari array itu. Waktu, kaya di max, dia satu-satunya parameter, dia bakal megang semua argumen.
Kamu bisa pakai notasi tiga titik serupa buat manggil fungsi dengan array argumen.
let numbers = [5, 1, 7];
console.log(max(...numbers));
// → 7Ini “nyebarin” (spread) array-nya ke pemanggilan fungsi, ngasih elemen-elemennya sebagai argumen terpisah. Bisa aja nyertain array kaya gitu bareng argumen lain, kaya di max(9, ...numbers, 2).
Notasi array kurung siku juga ngebolehin operator tiga titik buat nyebarin array lain ke array baru:
let words = ["never", "fully"];
console.log(["will", ...words, "understand"]);
// → ["will", "never", "fully", "understand"]Ini jalan bahkan di objek kurung kurawal, di mana dia nambahin semua properti dari objek lain. Kalau properti ditambahin beberapa kali, nilai terakhir yang ditambahin yang menang:
let coordinates = {x: 10, y: 0};
console.log({...coordinates, y: 5, z: 1});
// → {x: 10, y: 5, z: 1}Objek Math
Kaya yang udah kita lihat, Math adalah kantong campuran fungsi utilitas terkait angka kaya Math.max (maksimum), Math.min (minimum), dan Math.sqrt (akar kuadrat).
Objek Math dipakai sebagai wadah buat ngelompokin sekumpulan fungsionalitas terkait. Cuma ada satu objek Math, dan dia hampir gak pernah berguna sebagai nilai. Sebaliknya, dia nyediain namespace biar semua fungsi dan nilai ini gak harus jadi binding global.
Punya terlalu banyak binding global “mencemari” namespace. Makin banyak nama yang udah diambil, makin mungkin kamu gak sengaja nimpa nilai binding yang udah ada. Misalnya, gak mustahil kamu bakal mau ngasih nama sesuatu max di salah satu programmu. Karena fungsi max bawaan JavaScript disimpan dengan aman di dalam objek Math, kamu gak perlu khawatir nimpanya.
Banyak bahasa bakal ngehentiin kamu, atau setidaknya memperingatkan, waktu kamu mendefinisikan binding dengan nama yang udah diambil. JavaScript ngelakuin ini buat binding yang kamu deklarasikan dengan let atau const tapi, anehnya, gak buat binding standar maupun binding yang dideklarasikan dengan var atau function.
Balik ke objek Math. Kalau kamu perlu ngelakuin trigonometri, Math bisa bantu. Dia berisi cos (kosinus), sin (sinus), dan tan (tangen), juga fungsi inversnya, masing-masing acos, asin, dan atan. Angka π (pi), atau setidaknya pendekatan terdekat yang muat di angka JavaScript, tersedia sebagai Math.PI. Ada tradisi pemrograman lama buat nulis nama nilai konstan dengan huruf besar semua.
function randomPointOnCircle(radius) {
let angle = Math.random() * 2 * Math.PI;
return {x: radius * Math.cos(angle),
y: radius * Math.sin(angle)};
}
console.log(randomPointOnCircle(2));
// → {x: 0.3667, y: 1.966}Kalau kamu gak akrab sama sinus dan kosinus, jangan khawatir. Saya bakal ngejelasinnya waktu dipakai di Bab 14.
Contoh sebelumnya pakai Math.random. Ini fungsi yang ngembaliin angka pseudo-acak baru antara 0 (inklusif) dan 1 (eksklusif) setiap kali kamu manggilnya:
console.log(Math.random());
// → 0.36993729369714856
console.log(Math.random());
// → 0.727367032552138
console.log(Math.random());
// → 0.40180766698904335Walaupun komputer itu mesin deterministik, mereka selalu bereaksi dengan cara yang sama kalau dikasih input yang sama, bisa aja bikin mereka ngehasilin angka yang keliatan acak. Buat itu, mesinnya nyimpen suatu nilai tersembunyi, dan setiap kali kamu minta angka acak baru, dia ngelakuin perhitungan rumit ke nilai tersembunyi ini buat bikin nilai baru. Dia nyimpen nilai baru dan ngembaliin suatu angka yang diturunin darinya. Dengan cara itu, dia bisa ngehasilin angka yang selalu baru dan susah ditebak dengan cara yang keliatan acak.
Kalau kita mau bilangan bulat acak bukannya pecahan, kita bisa pakai Math.floor (yang ngebulatin ke bawah ke bilangan bulat terdekat) ke hasil Math.random:
console.log(Math.floor(Math.random() * 10));
// → 2Ngaliin angka acaknya dengan 10 ngasih kita angka yang lebih besar atau sama dengan 0 dan di bawah 10. Karena Math.floor ngebulatin ke bawah, ekspresi ini bakal ngehasilin, dengan peluang sama, angka apa pun dari 0 sampai 9.
Ada juga fungsi Math.ceil (buat “ceiling”, yang ngebulatin ke atas ke bilangan bulat), Math.round (ke bilangan bulat terdekat), dan Math.abs, yang ngambil nilai absolut dari angka, artinya dia menegasi nilai negatif tapi ngebiarin yang positif apa adanya.
Destructuring
Kita balik ke fungsi phi sebentar.
function phi(table) {
return (table[3] * table[0] - table[2] * table[1]) /
Math.sqrt((table[2] + table[3]) *
(table[0] + table[1]) *
(table[1] + table[3]) *
(table[0] + table[2]));
}Salah satu alasan fungsi ini canggung dibaca adalah kita punya binding yang nunjuk ke array kita, tapi kita jauh lebih suka punya binding buat elemen-elemen array-nya, yaitu, let n00 = table[0] dan seterusnya. Untungnya, ada cara ringkas buat ngelakuin ini di JavaScript:
function phi([n00, n01, n10, n11]) {
return (n11 * n00 - n10 * n01) /
Math.sqrt((n10 + n11) * (n00 + n01) *
(n01 + n11) * (n00 + n10));
}Ini juga jalan buat binding yang dibikin dengan let, var, atau const. Kalau kamu tahu nilai yang kamu ikat adalah array, kamu bisa pakai kurung siku buat “ngelihat ke dalam” nilainya, ngikat isinya.
Trik serupa jalan buat objek, pakai kurung kurawal bukannya kurung siku.
let {name} = {name: "Faraji", age: 23};
console.log(name);
// → FarajiCatatan, kalau kamu nyoba destructure null atau undefined, kamu dapet error, sama kaya kalau kamu langsung nyoba ngakses properti dari nilai-nilai itu.
Akses Properti Opsional
Waktu kamu gak yakin apakah nilai tertentu ngehasilin objek, tapi tetap mau baca properti darinya kalau iya, kamu bisa pakai varian notasi titik: object?.property.
function city(object) {
return object.address?.city;
}
console.log(city({address: {city: "Toronto"}}));
// → Toronto
console.log(city({name: "Vera"}));
// → undefinedEkspresi a?.b artinya sama dengan a.b waktu a bukan null atau undefined. Waktu iya, dia dievaluasi jadi undefined. Ini bisa praktis waktu, kaya di contoh, kamu gak yakin properti tertentu ada atau waktu variabel mungkin megang nilai undefined.
Notasi serupa bisa dipakai dengan akses kurung siku, dan bahkan dengan pemanggilan fungsi, dengan naruh ?. di depan tanda kurung atau kurung sikunya:
console.log("string".notAMethod?.());
// → undefined
console.log({}.arrayProp?.[0]);
// → undefinedJSON
Karena properti menggenggam nilainya bukannya berisi nilainya, objek dan array disimpan di memori komputer sebagai deretan bit yang megang alamat, tempat di memori, dari isinya. Array dengan array lain di dalamnya terdiri dari (setidaknya) satu wilayah memori buat array dalam dan satu lagi buat array luar, yang berisi (antara lain) angka yang mewakili alamat array dalamnya.
Kalau kamu mau nyimpen data di file buat nanti atau ngirimnya ke komputer lain lewat jaringan, kamu harus entah gimana ngonversi kekusutan alamat memori ini jadi deskripsi yang bisa disimpan atau dikirim. Kamu bisa ngirim seluruh memori komputermu bareng alamat nilai yang kamu minati, saya rasa, tapi itu keliatannya bukan pendekatan terbaik.
Yang bisa kita lakuin adalah serialisasi datanya. Artinya dia dikonversi jadi deskripsi pipih. Format serialisasi yang populer disebut JSON (diucapkan “Jason”), singkatan dari JavaScript Object Notation. Dia dipakai luas sebagai format penyimpanan dan komunikasi data di web, bahkan dengan bahasa selain JavaScript.
JSON keliatan mirip cara JavaScript nulis array dan objek, dengan beberapa batasan. Semua nama properti harus dikelilingi kutip ganda, dan cuma ekspresi data sederhana yang dibolehin, gak ada pemanggilan fungsi, binding, atau apa pun yang melibatkan perhitungan beneran. Komentar gak dibolehin di JSON.
Entri jurnal mungkin keliatan kaya gini waktu diwakili sebagai data JSON:
{
"squirrel": false,
"events": ["work", "touched tree", "pizza", "running"]
}JavaScript ngasih kita fungsi JSON.stringify dan JSON.parse buat ngonversi data ke dan dari format ini. Yang pertama ngambil nilai JavaScript dan ngembaliin string berkode JSON. Yang kedua ngambil string kaya gitu dan ngonversinya ke nilai yang dia kodekan:
let string = JSON.stringify({squirrel: false,
events: ["weekend"]});
console.log(string);
// → {"squirrel":false,"events":["weekend"]}
console.log(JSON.parse(string).events);
// → ["weekend"]Ringkasan
Objek dan array nyediain cara buat ngelompokin beberapa nilai jadi satu nilai. Ini bikin kita bisa naruh sekumpulan hal terkait ke dalam tas dan lari-lari bawa tasnya bukannya merangkul semua benda individualnya dan nyoba megangnya terpisah.
Kebanyakan nilai di JavaScript punya properti, dengan pengecualian null dan undefined. Properti diakses pakai value.prop atau value["prop"]. Objek cenderung pakai nama buat propertinya dan nyimpen kurang lebih sekumpulan tetap dari mereka. Array, di sisi lain, biasanya berisi jumlah bervariasi dari nilai yang secara konseptual identik dan pakai angka (mulai dari 0) sebagai nama propertinya.
Ada beberapa properti bernama di array, kaya length dan sejumlah method. Method adalah fungsi yang tinggal di properti dan (biasanya) beraksi pada nilai yang jadi pemilik propertinya.
Kamu bisa iterasi array pakai jenis loop for khusus: for (let element of array).
Poin-poin pentingnya:
- Array itu daftar nilai berindeks mulai dari 0; objek itu kumpulan properti bernama. Keduanya diakses pakai
.atau[]. - Method array penting:
push/pop(akhir),unshift/shift(awal),indexOf/lastIndexOf,slice,concat,includes. - Method string penting:
slice,indexOf,trim,padStart,split/join,repeat,toUpperCase/toLowerCase. - Objek itu mutable dan dibandingin berdasarkan identitas; angka/string/Boolean itu immutable.
for (let x of koleksi)buat ngeloop elemen.Object.keysbuat daftar properti,inbuat ngecek keberadaan,deletebuat ngehapus....buat rest parameter, spread ke pemanggilan fungsi, array, dan objek.- Destructuring
[a, b] = arraydan{x, y} = objek; optional chaininga?.b. JSON.stringifydanJSON.parsebuat serialisasi data.
Latihan
Jumlah dari Sebuah Rentang
Pengenalan buku ini nyinggung yang berikut sebagai cara bagus buat ngitung jumlah dari rentang angka:
console.log(sum(range(1, 10)));Tulis fungsi range yang ngambil dua argumen, start dan end, dan ngembaliin array yang berisi semua angka dari start sampai dan termasuk end.
Berikutnya, tulis fungsi sum yang ngambil array angka dan ngembaliin jumlah angka-angka itu. Jalanin program contohnya dan lihat apakah dia beneran ngembaliin 55.
Sebagai tugas bonus, modifikasi fungsi range-mu buat ngambil argumen ketiga opsional yang nunjukin nilai “step” (langkah) yang dipakai waktu ngebangun array-nya. Kalau gak ada step yang dikasih, elemennya harus naik dengan kenaikan satu, sesuai perilaku lama. Pemanggilan range(1, 10, 2) harus ngembaliin [1, 3, 5, 7, 9]. Pastiin ini juga jalan dengan nilai step negatif jadi range(5, 2, -1) ngehasilin [5, 4, 3, 2].
// Kodemu di sini.
console.log(range(1, 10));
// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(range(5, 2, -1));
// → [5, 4, 3, 2]
console.log(sum(range(1, 10)));
// → 55Petunjuk:
Ngebangun array paling gampang dilakuin dengan pertama nginisialisasi binding ke [] (array kosong yang baru) dan berulang kali manggil method push-nya buat nambahin nilai. Jangan lupa ngembaliin array-nya di akhir fungsi.
Karena batas akhirnya inklusif, kamu perlu pakai operator <= bukannya < buat ngecek akhir loop-mu.
Parameter step bisa jadi parameter opsional yang default-nya (pakai operator =) ke 1.
Bikin range paham nilai step negatif mungkin paling baik dilakuin dengan nulis dua loop terpisah, satu buat ngitung naik dan satu buat ngitung turun, karena perbandingan yang ngecek apakah loop-nya selesai perlu >= bukannya <= waktu ngitung turun.
Mungkin juga berguna pakai step default yang berbeda, yaitu, -1, waktu akhir rentangnya lebih kecil dari awalnya. Dengan cara itu, range(5, 2) ngembaliin sesuatu yang bermakna bukannya nyangkut di infinite loop. Bisa aja ngerujuk parameter sebelumnya di nilai default sebuah parameter.
Solusi:
function range(start, end, step = start < end ? 1 : -1) {
let array = [];
if (step > 0) {
for (let i = start; i <= end; i += step) array.push(i);
} else {
for (let i = start; i >= end; i += step) array.push(i);
}
return array;
}
function sum(array) {
let total = 0;
for (let value of array) {
total += value;
}
return total;
}
console.log(range(1, 10))
// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(range(5, 2, -1));
// → [5, 4, 3, 2]
console.log(sum(range(1, 10)));
// → 55Ngebalik Array
Array punya method reverse yang ngubah array dengan ngebalik urutan kemunculan elemennya. Buat latihan ini, tulis dua fungsi, reverseArray dan reverseArrayInPlace. Yang pertama, reverseArray, harus ngambil array sebagai argumennya dan ngehasilin array baru yang punya elemen yang sama dalam urutan terbalik. Yang kedua, reverseArrayInPlace, harus ngelakuin yang dilakuin method reverse: memodifikasi array yang dikasih sebagai argumennya dengan ngebalik elemennya. Keduanya gak boleh pakai method reverse standar.
Mengingat catatan soal side effect dan fungsi murni di bab sebelumnya, varian mana yang kamu harapkan berguna di lebih banyak situasi? Mana yang jalan lebih cepat?
// Kodemu di sini.
let myArray = ["A", "B", "C"];
console.log(reverseArray(myArray));
// → ["C", "B", "A"];
console.log(myArray);
// → ["A", "B", "C"];
let arrayValue = [1, 2, 3, 4, 5];
reverseArrayInPlace(arrayValue);
console.log(arrayValue);
// → [5, 4, 3, 2, 1]Petunjuk:
Ada dua cara jelas buat ngimplementasiin reverseArray. Yang pertama adalah cukup ngelewatin array input dari depan ke belakang dan pakai method unshift di array baru buat nyisipin setiap elemen di awalnya. Yang kedua adalah ngeloop array input mundur dan pakai method push. Iterasi array mundur butuh spesifikasi for yang (agak canggung), kaya (let i = array.length - 1; i >= 0; i--).
Ngebalik array di tempat lebih susah. Kamu harus hati-hati gak nimpa elemen yang bakal kamu butuhin nanti. Pakai reverseArray atau dengan cara lain nyalin seluruh array (array.slice() cara bagus buat nyalin array) jalan tapi curang.
Triknya adalah nuker elemen pertama dan terakhir, lalu kedua dan kedua dari terakhir, dan seterusnya. Kamu bisa ngelakuin ini dengan ngeloop setengah panjang array (pakai Math.floor buat ngebulatin ke bawah, kamu gak perlu nyentuh elemen tengah di array dengan jumlah elemen ganjil) dan nuker elemen di posisi i dengan yang di posisi array.length - 1 - i. Kamu bisa pakai binding lokal buat sebentar megang salah satu elemennya, nimpa yang itu dengan bayangan cerminnya, lalu naruh nilai dari binding lokal di tempat bayangan cerminnya tadi.
Solusi:
function reverseArray(array) {
let output = [];
for (let i = array.length - 1; i >= 0; i--) {
output.push(array[i]);
}
return output;
}
function reverseArrayInPlace(array) {
for (let i = 0; i < Math.floor(array.length / 2); i++) {
let old = array[i];
array[i] = array[array.length - 1 - i];
array[array.length - 1 - i] = old;
}
return array;
}
console.log(reverseArray(["A", "B", "C"]));
// → ["C", "B", "A"];
let arrayValue = [1, 2, 3, 4, 5];
reverseArrayInPlace(arrayValue);
console.log(arrayValue);
// → [5, 4, 3, 2, 1]Sebuah List
Sebagai gumpalan nilai generik, objek bisa dipakai buat ngebangun segala macam struktur data. Struktur data yang umum adalah list (jangan ketuker sama array). List adalah sekumpulan objek bersarang, dengan objek pertama megang referensi ke yang kedua, yang kedua ke yang ketiga, dan seterusnya:
let list = {
value: 1,
rest: {
value: 2,
rest: {
value: 3,
rest: null
}
}
};Objek-objek hasilnya membentuk rantai, kaya yang ditunjukin di diagram berikut:
Hal bagus soal list adalah mereka bisa berbagi bagian dari strukturnya. Misalnya, kalau saya bikin dua nilai baru {value: 0, rest: list} dan {value: -1, rest: list} (dengan list ngerujuk ke binding yang didefinisikan sebelumnya), keduanya list independen, tapi mereka berbagi struktur yang membentuk tiga elemen terakhirnya. List aslinya juga masih list tiga elemen yang valid.
Tulis fungsi arrayToList yang ngebangun struktur list kaya yang ditunjukin waktu dikasih [1, 2, 3] sebagai argumen. Juga tulis fungsi listToArray yang ngehasilin array dari list. Tambahin fungsi pembantu prepend, yang ngambil elemen dan list dan bikin list baru yang nambahin elemennya ke depan list input, dan nth, yang ngambil list dan angka dan ngembaliin elemen di posisi yang dikasih di list (dengan nol ngerujuk ke elemen pertama) atau undefined kalau gak ada elemen kaya gitu.
Kalau belum, tulis juga versi rekursif dari nth.
// Kodemu di sini.
console.log(arrayToList([10, 20]));
// → {value: 10, rest: {value: 20, rest: null}}
console.log(listToArray(arrayToList([10, 20, 30])));
// → [10, 20, 30]
console.log(prepend(10, prepend(20, null)));
// → {value: 10, rest: {value: 20, rest: null}}
console.log(nth(arrayToList([10, 20, 30]), 1));
// → 20Petunjuk:
Ngebangun list lebih gampang kalau dilakuin dari belakang ke depan. Jadi arrayToList bisa iterasi array-nya mundur (lihat latihan sebelumnya) dan, buat setiap elemen, nambahin objek ke list. Kamu bisa pakai binding lokal buat megang bagian list yang udah dibangun sejauh ini dan pakai assignment kaya list = {value: X, rest: list} buat nambahin elemen.
Buat ngelewatin list (di listToArray dan nth), spesifikasi loop for kaya gini bisa dipakai:
for (let node = list; node; node = node.rest) {}Bisa kamu lihat cara kerjanya? Setiap iterasi loop, node nunjuk ke sublist saat ini, dan badannya bisa baca properti value-nya buat dapet elemen saat ini. Di akhir iterasi, node pindah ke sublist berikutnya. Waktu itu null, kita udah nyampe akhir list, dan loop-nya selesai.
Versi rekursif nth bakal, serupa, ngelihat bagian yang makin kecil dari “ekor” list dan di saat yang sama ngitung mundur indeksnya sampai nyampe nol, di titik mana dia bisa ngembaliin properti value dari node yang lagi dia lihat. Buat dapet elemen ke-nol dari list, kamu cukup ngambil properti value dari node kepalanya. Buat dapet elemen ke-N + 1, kamu ngambil elemen ke-N dari list yang ada di properti rest list ini.
Solusi:
function arrayToList(array) {
let list = null;
for (let i = array.length - 1; i >= 0; i--) {
list = {value: array[i], rest: list};
}
return list;
}
function listToArray(list) {
let array = [];
for (let node = list; node; node = node.rest) {
array.push(node.value);
}
return array;
}
function prepend(value, list) {
return {value, rest: list};
}
function nth(list, n) {
if (!list) return undefined;
else if (n == 0) return list.value;
else return nth(list.rest, n - 1);
}
console.log(arrayToList([10, 20]));
// → {value: 10, rest: {value: 20, rest: null}}
console.log(listToArray(arrayToList([10, 20, 30])));
// → [10, 20, 30]
console.log(prepend(10, prepend(20, null)));
// → {value: 10, rest: {value: 20, rest: null}}
console.log(nth(arrayToList([10, 20, 30]), 1));
// → 20Perbandingan Dalam (Deep Comparison)
Operator == ngebandingin objek berdasarkan identitas, tapi kadang kamu lebih suka ngebandingin nilai dari properti aktualnya.
Tulis fungsi deepEqual yang ngambil dua nilai dan ngembaliin true cuma kalau mereka nilai yang sama atau objek dengan properti yang sama, di mana nilai propertinya sama waktu dibandingin dengan pemanggilan rekursif ke deepEqual.
Buat tahu apakah nilai harus dibandingin langsung (pakai operator === buat itu) atau propertinya yang dibandingin, kamu bisa pakai operator typeof. Kalau dia ngehasilin "object" buat kedua nilai, kamu harus ngelakuin perbandingan dalam. Tapi kamu harus memperhitungkan satu pengecualian konyol: karena kecelakaan sejarah, typeof null juga ngehasilin "object".
Fungsi Object.keys bakal berguna waktu kamu perlu ngelewatin properti objek buat ngebandinginnya.
// Kodemu di sini.
let obj = {here: {is: "an"}, object: 2};
console.log(deepEqual(obj, obj));
// → true
console.log(deepEqual(obj, {here: 1, object: 2}));
// → false
console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));
// → truePetunjuk:
Tesmu buat apakah kamu nanganin objek beneran bakal keliatan kaya typeof x == "object" && x != null. Hati-hati buat ngebandingin properti cuma waktu kedua argumennya objek. Di semua kasus lain kamu bisa langsung ngembaliin hasil dari nerapin ===.
Pakai Object.keys buat ngelewatin propertinya. Kamu perlu ngetes apakah kedua objek punya kumpulan nama properti yang sama dan apakah properti-properti itu punya nilai identik. Salah satu caranya adalah mastiin kedua objek punya jumlah properti yang sama (panjang daftar propertinya sama). Lalu, waktu ngeloop properti salah satu objek buat ngebandinginnya, selalu pastiin dulu yang lain beneran punya properti dengan nama itu. Kalau mereka punya jumlah properti yang sama dan semua properti di yang satu juga ada di yang lain, mereka punya kumpulan nama properti yang sama.
Ngembaliin nilai yang benar dari fungsinya paling baik dilakuin dengan langsung ngembaliin false waktu ketidakcocokan ditemuin dan ngembaliin true di akhir fungsi.
Solusi:
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || typeof a != "object" ||
b == null || typeof b != "object") return false;
let keysA = Object.keys(a), keysB = Object.keys(b);
if (keysA.length != keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
}
return true;
}
let obj = {here: {is: "an"}, object: 2};
console.log(deepEqual(obj, obj));
// → true
console.log(deepEqual(obj, {here: 1, object: 2}));
// → false
console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));
// → true