Bab 3 dari Eloquent JavaScript. Sebelumnya: 02 Struktur Program. Berikutnya: 04 Struktur Data Objek dan Array.

Orang mikir ilmu komputer itu seni para jenius, tapi kenyataannya justru sebaliknya, cuma banyak orang ngelakuin hal-hal yang saling membangun satu sama lain, kaya tembok dari batu-batu kecil.

— Donald Knuth

Ilustrasi daun pakis dengan bentuk fraktal, lebah di latar belakang

Fungsi adalah salah satu alat paling sentral di pemrograman JavaScript. Konsep ngebungkus potongan program dalam sebuah nilai punya banyak kegunaan. Dia ngasih kita cara buat nyusun program yang lebih besar, ngurangin pengulangan, ngaitin nama dengan subprogram, dan ngisolasi subprogram-subprogram ini satu sama lain.

Penerapan fungsi yang paling jelas adalah mendefinisikan kosakata baru. Bikin kata baru di prosa biasanya gaya yang buruk, tapi di pemrograman, ini gak bisa dihindari.

Penutur dewasa bahasa Inggris biasanya punya sekitar 20.000 kata di kosakata mereka. Sedikit bahasa pemrograman yang datang dengan 20.000 perintah bawaan. Dan kosakata yang tersedia cenderung didefinisikan lebih presisi, dan karena itu kurang fleksibel, dibanding bahasa manusia. Makanya, kita harus ngenalin kata-kata baru biar gak bertele-tele.

Mendefinisikan Fungsi

Definisi fungsi adalah binding biasa di mana nilai bindingnya adalah fungsi. Misalnya, kode ini mendefinisikan square buat nunjuk ke fungsi yang ngehasilin kuadrat dari angka yang dikasih:

const square = function(x) {
  return x * x;
};
 
console.log(square(12));
// → 144

Fungsi dibikin dengan ekspresi yang dimulai dengan keyword function. Fungsi punya sekumpulan parameter (di kasus ini, cuma x) dan sebuah badan (body), yang berisi statement-statement yang bakal dieksekusi waktu fungsinya dipanggil. Badan fungsi yang dibikin dengan cara ini harus selalu dibungkus kurung kurawal, bahkan kalau cuma terdiri dari satu statement.

Fungsi bisa punya beberapa parameter atau gak ada parameter sama sekali. Di contoh berikut, makeNoise gak nyebutin nama parameter apa pun, sedangkan roundTo (yang ngebulatin n ke kelipatan step terdekat) nyebutin dua:

const makeNoise = function() {
  console.log("Pling!");
};
 
makeNoise();
// → Pling!
 
const roundTo = function(n, step) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};
 
console.log(roundTo(23, 10));
// → 20

Beberapa fungsi, kaya roundTo dan square, ngehasilin nilai, dan beberapa gak, kaya makeNoise, yang satu-satunya hasilnya adalah side effect. Statement return nentuin nilai yang dikembaliin fungsi. Waktu kontrol nemuin statement kaya gitu, dia langsung lompat keluar dari fungsi saat ini dan ngasih nilai yang dikembaliin ke kode yang manggil fungsinya. Keyword return tanpa ekspresi setelahnya bakal bikin fungsi ngembaliin undefined. Fungsi yang gak punya statement return sama sekali, kaya makeNoise, juga ngembaliin undefined.

Parameter fungsi berperilaku kaya binding biasa, tapi nilai awalnya dikasih sama pemanggil fungsi, bukan kode di dalam fungsinya sendiri.

Binding dan Scope

Setiap binding punya scope (lingkup), yaitu bagian program di mana bindingnya terlihat. Buat binding yang didefinisikan di luar fungsi, block, atau modul mana pun (lihat Bab 10), scope-nya adalah seluruh program, kamu bisa ngerujuk binding kaya gitu di mana pun kamu mau. Ini disebut global.

Binding yang dibikin buat parameter fungsi atau dideklarasikan di dalam fungsi cuma bisa dirujuk di dalam fungsi itu, jadi mereka dikenal sebagai binding lokal. Setiap kali fungsinya dipanggil, instance baru dari binding-binding ini dibikin. Ini ngasih sedikit isolasi antar fungsi, setiap pemanggilan fungsi beraksi di dunia kecilnya sendiri (environment lokalnya) dan sering bisa dipahami tanpa perlu tahu banyak soal apa yang terjadi di environment global.

Binding yang dideklarasikan dengan let dan const sebenernya lokal ke block tempat mereka dideklarasikan, jadi kalau kamu bikin salah satunya di dalam loop, kode sebelum dan sesudah loop gak bisa “ngelihat” dia. Di JavaScript sebelum 2015, cuma fungsi yang bikin scope baru, jadi binding gaya lama, yang dibikin dengan keyword var, terlihat di seluruh fungsi tempat mereka muncul, atau di seluruh scope global, kalau mereka gak ada di dalam fungsi.

let x = 10;   // global
if (true) {
  let y = 20; // lokal ke block
  var z = 30; // juga global
}

Setiap scope bisa “ngelihat keluar” ke scope di sekitarnya, jadi x terlihat di dalam block di contoh tadi. Pengecualiannya adalah kalau beberapa binding punya nama yang sama, di kasus itu, kode cuma bisa ngelihat yang paling dalam. Misalnya, waktu kode di dalam fungsi halve ngerujuk ke n, dia ngelihat n miliknya sendiri, bukan n global.

const halve = function(n) {
  return n / 2;
};
 
let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

Scope Bersarang

JavaScript gak cuma ngebedain binding global dan lokal. Block dan fungsi bisa dibikin di dalam block dan fungsi lain, ngehasilin beberapa tingkat lokalitas.

Misalnya, fungsi ini, yang ngeluarin bahan-bahan yang dibutuhin buat bikin satu porsi hummus, punya fungsi lain di dalamnya:

const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

Kode di dalam fungsi ingredient bisa ngelihat binding factor dari fungsi luar, tapi binding lokalnya, kaya unit atau ingredientAmount, gak terlihat di fungsi luar.

Kumpulan binding yang terlihat di dalam sebuah block ditentuin sama tempat block itu di teks program. Setiap scope lokal juga bisa ngelihat semua scope lokal yang ngelingkupinya, dan semua scope bisa ngelihat scope global. Pendekatan visibilitas binding ini disebut lexical scoping.

Fungsi sebagai Nilai

Binding fungsi biasanya cuma bertindak sebagai nama buat potongan program tertentu. Binding kaya gitu didefinisikan sekali dan gak pernah diubah. Ini bikin gampang ketuker antara fungsi dan namanya.

Tapi keduanya berbeda. Nilai fungsi bisa ngelakuin semua hal yang bisa dilakuin nilai lain, kamu bisa pakai dia di ekspresi apa pun, gak cuma manggilnya. Bisa aja nyimpen nilai fungsi di binding baru, ngasihnya sebagai argumen ke fungsi lain, dan seterusnya. Sama juga, binding yang megang fungsi tetap cuma binding biasa dan bisa, kalau bukan konstanta, dikasih nilai baru, kaya gini:

let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* gak ngapa-ngapain */};
}

Di Bab 5, kita bakal ngebahas hal-hal menarik yang bisa kita lakuin dengan ngasih nilai fungsi ke fungsi lain.

Notasi Deklarasi

Ada cara yang sedikit lebih pendek buat bikin binding fungsi. Waktu keyword function dipakai di awal statement, dia bekerja beda:

function square(x) {
  return x * x;
}

Ini deklarasi fungsi. Statement ini mendefinisikan binding square dan nunjukin dia ke fungsi yang dikasih. Ini sedikit lebih gampang ditulis dan gak butuh titik koma setelah fungsinya.

Ada satu kehalusan dengan bentuk definisi fungsi ini.

console.log("The future says:", future());
 
function future() {
  return "You'll never have flying cars";
}

Kode di atas jalan, walaupun fungsinya didefinisikan di bawah kode yang pakai dia. Deklarasi fungsi bukan bagian dari alur kontrol biasa dari atas ke bawah. Mereka secara konseptual dipindahin ke atas scope-nya dan bisa dipakai semua kode di scope itu. Ini kadang berguna karena ngasih kebebasan buat ngurutin kode dengan cara yang keliatan paling jelas, tanpa khawatir harus mendefinisikan semua fungsi sebelum dipakai.

Arrow Function

Ada notasi ketiga buat fungsi, yang keliatannya sangat beda dari yang lain. Bukannya keyword function, dia pakai panah (=>) yang terdiri dari tanda sama dengan dan karakter lebih besar (jangan ketuker sama operator lebih-besar-atau-sama-dengan, yang ditulis >=):

const roundTo = (n, step) => {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

Panahnya ada setelah daftar parameter dan diikuti badan fungsinya. Dia nyatain sesuatu kaya “input ini (parameternya) ngehasilin hasil ini (badannya)”.

Kalau cuma ada satu nama parameter, kamu bisa ngilangin tanda kurung di sekitar daftar parameternya. Kalau badannya cuma satu ekspresi bukannya block dalam kurung kurawal, ekspresi itu bakal dikembaliin dari fungsinya. Jadi, dua definisi square ini ngelakuin hal yang sama:

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

Kalau arrow function gak punya parameter sama sekali, daftar parameternya cuma sepasang tanda kurung kosong.

const horn = () => {
  console.log("Toot");
};

Gak ada alasan mendalam buat punya arrow function dan ekspresi function sekaligus di bahasa ini. Selain satu detail kecil, yang bakal kita bahas di Bab 6, mereka ngelakuin hal yang sama. Arrow function ditambahin tahun 2015, terutama biar bisa nulis ekspresi fungsi kecil dengan cara yang gak bertele-tele. Kita bakal sering pakai mereka di Bab 5.

Call Stack

Cara kontrol mengalir lewat fungsi agak rumit. Kita lihat lebih dekat. Ini program sederhana yang bikin beberapa pemanggilan fungsi:

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

Jalannya program ini kurang lebih kaya gini: pemanggilan greet bikin kontrol lompat ke awal fungsi itu (baris 2). Fungsinya manggil console.log, yang ngambil kontrol, ngelakuin tugasnya, lalu ngembaliin kontrol ke baris 2. Di sana, dia nyampe akhir fungsi greet, jadi dia balik ke tempat yang manggilnya, baris 4. Baris setelahnya manggil console.log lagi. Setelah itu balik, programnya nyampe akhirnya.

Kita bisa nunjukin alur kontrolnya secara skematis kaya gini:

gak di dalam fungsi
  di dalam greet
    di dalam console.log
  di dalam greet
gak di dalam fungsi
  di dalam console.log
gak di dalam fungsi

Karena fungsi harus lompat balik ke tempat yang manggilnya waktu dia return, komputer harus ngingat konteks tempat pemanggilan terjadi. Di satu kasus, console.log harus balik ke fungsi greet waktu dia selesai. Di kasus lain, dia balik ke akhir program.

Tempat komputer nyimpen konteks ini adalah call stack. Setiap kali fungsi dipanggil, konteks saat ini disimpan di atas stack ini. Waktu fungsi return, dia ngehapus konteks paling atas dari stack dan pakai konteks itu buat ngelanjutin eksekusi.

Nyimpen stack ini butuh ruang di memori komputer. Waktu stack-nya tumbuh terlalu besar, komputer bakal gagal dengan pesan kaya “out of stack space” atau “too much recursion”. Kode berikut ngilustrasiin ini dengan nanya pertanyaan yang sangat susah ke komputer yang bikin bolak-balik tak terbatas antara dua fungsi. Atau lebih tepatnya, dia bakal tak terbatas, kalau komputernya punya stack tak terbatas. Kenyataannya, kita bakal kehabisan ruang, atau “blow the stack” (meledakkan stack).

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??

Argumen Opsional

Kode berikut diperbolehkan dan dieksekusi tanpa masalah:

function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

Kita mendefinisikan square cuma dengan satu parameter. Tapi waktu kita manggilnya dengan tiga, bahasanya gak protes. Dia ngabaikan argumen tambahannya dan ngitung kuadrat dari yang pertama.

JavaScript sangat berpikiran terbuka soal jumlah argumen yang bisa kamu kasih ke fungsi. Kalau kamu ngasih terlalu banyak, yang lebih diabaikan. Kalau kamu ngasih terlalu sedikit, parameter yang hilang dikasih nilai undefined.

Sisi buruknya adalah bisa aja, bahkan mungkin banget, kamu gak sengaja ngasih jumlah argumen yang salah ke fungsi. Dan gak ada yang bakal ngasih tahu kamu. Sisi baiknya adalah kamu bisa pakai perilaku ini buat ngebolehin fungsi dipanggil dengan jumlah argumen yang berbeda. Misalnya, fungsi minus ini nyoba niru operator - dengan beraksi pada satu atau dua argumen:

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}
 
console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

Kalau kamu nulis operator = setelah parameter, diikuti ekspresi, nilai ekspresi itu bakal ngegantiin argumennya kalau gak dikasih. Misalnya, versi roundTo ini bikin argumen keduanya opsional. Kalau kamu gak nyediainnya atau ngasih nilai undefined, dia bakal default ke satu:

function roundTo(n, step = 1) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};
 
console.log(roundTo(4.5));
// → 5
console.log(roundTo(4.5, 2));
// → 4

Bab berikutnya bakal ngenalin cara badan fungsi bisa ngakses seluruh daftar argumen yang dikasih ke dia. Ini berguna karena ngebolehin fungsi nerima berapa pun argumen. Misalnya, console.log ngelakuin ini, ngeluarin semua nilai yang dikasih ke dia:

console.log("C", "O", 2);
// → C O 2

Closure

Kemampuan memperlakukan fungsi sebagai nilai, dikombinasikan dengan fakta bahwa binding lokal dibikin ulang setiap kali fungsi dipanggil, memunculkan pertanyaan menarik: Apa yang terjadi ke binding lokal waktu pemanggilan fungsi yang bikin mereka udah gak aktif lagi?

Kode berikut nunjukin contoh soal ini. Dia mendefinisikan fungsi, wrapValue, yang bikin binding lokal. Lalu dia ngembaliin fungsi yang ngakses dan ngembaliin binding lokal ini.

function wrapValue(n) {
  let local = n;
  return () => local;
}
 
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

Ini diperbolehkan dan jalan sesuai harapan, kedua instance bindingnya masih bisa diakses. Situasi ini adalah demonstrasi yang bagus dari fakta bahwa binding lokal dibikin baru buat setiap pemanggilan, dan pemanggilan yang berbeda gak saling ngaruh ke binding lokal masing-masing.

Fitur ini, bisa ngerujuk ke instance tertentu dari binding lokal di scope yang ngelingkupinya, disebut closure. Fungsi yang ngerujuk binding dari scope lokal di sekitarnya disebut sebuah closure. Perilaku ini bukan cuma ngebebasin kamu dari keharusan khawatir soal masa hidup binding tapi juga bikin mungkin pakai nilai fungsi dengan cara-cara yang kreatif.

Dengan sedikit perubahan, kita bisa ngubah contoh sebelumnya jadi cara buat bikin fungsi yang ngaliin dengan jumlah berapa pun.

function multiplier(factor) {
  return number => number * factor;
}
 
let twice = multiplier(2);
console.log(twice(5));
// → 10

Binding local yang eksplisit dari contoh wrapValue sebenernya gak dibutuhin karena parameter sendiri udah binding lokal.

Mikirin program kaya gini butuh sedikit latihan. Model mental yang bagus adalah ngebayangin nilai fungsi sebagai sesuatu yang berisi kode di badannya sekaligus environment tempat dia dibikin. Waktu dipanggil, badan fungsi ngelihat environment tempat dia dibikin, bukan environment tempat dia dipanggil.

Di contoh sebelumnya, multiplier dipanggil dan bikin environment di mana parameter factor-nya terikat ke 2. Nilai fungsi yang dia kembaliin, yang disimpan di twice, ngingat environment ini jadi waktu dia dipanggil, dia ngaliin argumennya dengan 2.

Rekursi

Sangat boleh buat fungsi manggil dirinya sendiri, selama dia gak ngelakuinnya terlalu sering sampai stack-nya overflow. Fungsi yang manggil dirinya sendiri disebut rekursif. Rekursi ngebolehin beberapa fungsi ditulis dengan gaya yang berbeda. Ambil contoh fungsi power ini, yang ngelakuin hal yang sama dengan operator ** (pemangkatan):

function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}
 
console.log(power(2, 3));
// → 8

Ini agak dekat sama cara matematikawan mendefinisikan pemangkatan dan bisa dibilang ngejelasin konsepnya lebih jelas daripada loop yang kita pakai di Bab 2. Fungsinya manggil dirinya sendiri berkali-kali dengan eksponen yang makin kecil buat nyapai perkalian berulang.

Tapi, implementasi ini punya satu masalah: di implementasi JavaScript pada umumnya, dia sekitar tiga kali lebih lambat dari versi yang pakai loop for. Ngejalanin loop sederhana umumnya lebih murah daripada manggil fungsi berkali-kali.

Dilema kecepatan versus keanggunan ini menarik. Kamu bisa ngelihatnya sebagai semacam kontinum antara ramah-manusia dan ramah-mesin. Hampir semua program bisa dibikin lebih cepat dengan bikinnya lebih besar dan lebih berbelit. Programmer harus nemuin keseimbangan yang tepat.

Di kasus fungsi power, versi yang gak anggun (looping) masih cukup sederhana dan gampang dibaca. Gak terlalu masuk akal buat ngegantinya dengan fungsi rekursif. Tapi sering kali, program nanganin konsep yang begitu rumit sampai ngorbanin sedikit efisiensi biar programnya lebih lugas itu berguna.

Khawatir soal efisiensi bisa jadi gangguan. Itu faktor lain lagi yang bikin desain program rumit, dan waktu kamu ngelakuin sesuatu yang udah susah, hal tambahan yang harus dikhawatirin itu bisa bikin lumpuh.

Makanya, kamu umumnya sebaiknya mulai dengan nulis sesuatu yang benar dan gampang dipahami. Kalau kamu khawatir itu terlalu lambat, yang biasanya gak, karena kebanyakan kode gak dieksekusi cukup sering buat makan waktu yang signifikan, kamu bisa ngukur setelahnya dan memperbaikinya kalau perlu.

Rekursi gak selalu cuma alternatif yang gak efisien dari looping. Beberapa masalah beneran lebih gampang diselesaiin dengan rekursi daripada loop. Paling sering ini masalah yang butuh ngeksplorasi atau ngeproses beberapa “cabang”, yang masing-masing mungkin bercabang lagi jadi lebih banyak cabang.

Bayangin teka-teki ini: dengan mulai dari angka 1 dan berulang kali nambahin 5 atau ngaliin 3, sekumpulan angka tak terbatas bisa dihasilkan. Gimana kamu bakal nulis fungsi yang, dikasih sebuah angka, nyoba nemuin urutan penjumlahan dan perkalian kaya gitu yang ngehasilin angka itu? Misalnya, angka 13 bisa dicapai dengan pertama ngaliin 3 lalu nambahin 5 dua kali, sedangkan angka 15 gak bisa dicapai sama sekali.

Ini solusi rekursifnya:

function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ??
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}
 
console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

Catatan, program ini gak harus nemuin urutan operasi yang terpendek. Dia puas kalau nemuin urutan apa pun.

Gak apa-apa kalau kamu gak langsung ngerti cara kerja kode ini. Kita bahas pelan-pelan karena ini latihan yang bagus buat berpikir rekursif.

Fungsi dalam find yang ngelakuin rekursi sebenarnya. Dia ngambil dua argumen: angka saat ini dan string yang nyatet gimana kita nyampe angka ini. Kalau dia nemuin solusi, dia ngembaliin string yang nunjukin cara nyampe target. Kalau dia gak bisa nemuin solusi mulai dari angka ini, dia ngembaliin null.

Buat ngelakuin ini, fungsinya ngelakuin salah satu dari tiga aksi. Kalau angka saat ini adalah angka target, history saat ini adalah cara nyampe target itu, jadi dia dikembaliin. Kalau angka saat ini lebih besar dari target, gak ada gunanya ngeksplorasi cabang ini lebih jauh karena nambahin maupun ngaliin cuma bakal bikin angkanya lebih besar, jadi dia ngembaliin null. Terakhir, kalau kita masih di bawah angka target, fungsinya nyoba kedua jalur yang mungkin dari angka saat ini dengan manggil dirinya sendiri dua kali, sekali buat penjumlahan dan sekali buat perkalian. Kalau pemanggilan pertama ngembaliin sesuatu yang bukan null, itu yang dikembaliin. Kalau gak, pemanggilan kedua yang dikembaliin, terlepas dari apakah dia ngehasilin string atau null.

Buat lebih paham gimana fungsi ini ngehasilin efek yang kita cari, kita lihat semua pemanggilan find yang terjadi waktu nyari solusi buat angka 13:

find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        kebesaran
      find(33, "(((1 + 5) + 5) * 3)")
        kebesaran
    find(18, "((1 + 5) * 3)")
      kebesaran
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        ketemu!

Indentasinya nunjukin kedalaman call stack. Pertama kali find dipanggil, fungsinya mulai dengan manggil dirinya sendiri buat ngeksplorasi solusi yang dimulai dengan (1 + 5). Pemanggilan itu bakal rekursi lebih jauh buat ngeksplorasi setiap solusi lanjutan yang ngehasilin angka kurang dari atau sama dengan angka target. Karena dia gak nemuin yang kena target, dia ngembaliin null ke pemanggilan pertama. Di sana operator ?? bikin pemanggilan yang ngeksplorasi (1 * 3) terjadi. Pencarian ini lebih beruntung, pemanggilan rekursif pertamanya, lewat pemanggilan rekursif lain lagi, kena angka target. Pemanggilan paling dalam itu ngembaliin string, dan setiap operator ?? di pemanggilan-pemanggilan tengah nerusin string itu, akhirnya ngembaliin solusinya.

Numbuhin Fungsi

Ada dua cara yang kurang lebih alami buat fungsi diperkenalkan ke dalam program.

Yang pertama terjadi waktu kamu nemuin dirimu nulis kode yang mirip berkali-kali. Kamu lebih suka gak ngelakuin itu, karena punya lebih banyak kode berarti lebih banyak ruang buat kesalahan bersembunyi dan lebih banyak bahan buat dibaca orang yang nyoba paham programnya. Jadi kamu ambil fungsionalitas yang berulang, cari nama yang bagus buat dia, dan taruh dia di sebuah fungsi.

Cara kedua adalah kamu nemuin kamu butuh fungsionalitas yang belum kamu tulis dan kedengarannya pantas punya fungsinya sendiri. Kamu mulai dengan ngasih nama fungsinya, lalu nulis badannya. Kamu bahkan mungkin mulai nulis kode yang pakai fungsinya sebelum kamu beneran mendefinisikan fungsinya sendiri.

Seberapa susah nemuin nama yang bagus buat sebuah fungsi adalah indikasi yang bagus soal seberapa jelas konsep yang kamu coba bungkus. Kita lihat contohnya.

Kita mau nulis program yang nyetak dua angka: jumlah sapi dan ayam di sebuah peternakan, dengan kata Cows dan Chickens setelahnya dan nol ditambahin di depan kedua angka biar mereka selalu tiga digit:

007 Cows
011 Chickens

Ini minta fungsi dengan dua argumen, jumlah sapi dan jumlah ayam. Ayo ngoding.

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);

Nulis .length setelah ekspresi string bakal ngasih kita panjang string itu. Jadi, loop while-nya terus nambahin nol di depan string angka sampai panjangnya minimal tiga karakter.

Misi selesai! Tapi tepat waktu kita mau ngirim kodenya ke peternaknya (bareng tagihan yang gede), dia nelepon dan bilang dia juga mulai melihara babi, dan bisakah kita memperluas softwarenya buat nyetak babi juga?

Tentu bisa. Tapi tepat waktu kita lagi nyalin dan nempel empat baris itu sekali lagi, kita berhenti dan mikir ulang. Pasti ada cara yang lebih baik. Ini percobaan pertama:

function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}
 
function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}
 
printFarmInventory(7, 11, 3);

Jalan! Tapi nama itu, printZeroPaddedWithLabel, agak canggung. Dia nyampur tiga hal, nyetak, nambahin nol, dan nambahin label, jadi satu fungsi.

Daripada ngangkat bagian yang berulang dari program kita secara utuh, kita coba ambil satu konsep aja:

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}
 
function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}
 
printFarmInventory(7, 16, 3);

Fungsi dengan nama yang bagus dan jelas kaya zeroPad bikin lebih gampang buat orang yang baca kodenya nangkep apa yang dia lakuin. Fungsi kaya gitu juga berguna di lebih banyak situasi daripada cuma program spesifik ini. Misalnya, kamu bisa pakai dia buat ngebantu nyetak tabel angka yang rapi sejajar.

Seberapa pintar dan serbaguna seharusnya fungsi kita? Kita bisa nulis apa aja, dari fungsi yang sangat sederhana yang cuma bisa nambahin nol ke angka biar tiga karakter lebarnya sampai sistem format angka umum yang rumit yang nanganin angka pecahan, angka negatif, penyelarasan titik desimal, padding dengan karakter berbeda, dan seterusnya.

Prinsip yang berguna adalah nahan diri dari nambahin kecerdikan kecuali kamu benar-benar yakin bakal butuh. Bisa menggoda buat nulis “framework” umum buat setiap potongan fungsionalitas yang kamu temuin. Lawan dorongan itu. Kamu gak bakal nyelesaiin kerjaan nyata apa pun, kamu bakal terlalu sibuk nulis kode yang gak pernah kamu pakai.

Fungsi dan Side Effect

Fungsi bisa dibagi kasar jadi yang dipanggil buat side effect-nya dan yang dipanggil buat nilai kembaliannya (walaupun bisa juga punya side effect sekaligus ngembaliin nilai).

Fungsi pembantu pertama di contoh peternakan, printZeroPaddedWithLabel, dipanggil buat side effect-nya: dia nyetak satu baris. Versi kedua, zeroPad, dipanggil buat nilai kembaliannya. Bukan kebetulan yang kedua berguna di lebih banyak situasi daripada yang pertama. Fungsi yang bikin nilai lebih gampang digabungin dengan cara baru daripada fungsi yang langsung ngelakuin side effect.

Fungsi murni (pure) adalah jenis fungsi penghasil nilai tertentu yang bukan cuma gak punya side effect tapi juga gak bergantung pada side effect dari kode lain, misalnya, dia gak baca binding global yang nilainya mungkin berubah. Fungsi murni punya sifat menyenangkan bahwa, waktu dipanggil dengan argumen yang sama, dia selalu ngehasilin nilai yang sama (dan gak ngelakuin hal lain). Pemanggilan fungsi kaya gitu bisa diganti dengan nilai kembaliannya tanpa ngubah makna kodenya. Waktu kamu gak yakin fungsi murni bekerja dengan benar, kamu bisa ngetesnya cukup dengan manggilnya dan tahu bahwa kalau dia jalan di konteks itu, dia bakal jalan di konteks mana pun. Fungsi yang gak murni cenderung butuh lebih banyak perancah buat dites.

Tetap aja, gak perlu ngerasa bersalah waktu nulis fungsi yang gak murni. Side effect sering berguna. Gak ada cara nulis versi murni dari console.log, misalnya, dan console.log itu bagus buat dimiliki. Beberapa operasi juga lebih gampang diekspresikan dengan cara yang efisien kalau kita pakai side effect.

Ringkasan

Bab ini ngajarin kamu cara nulis fungsimu sendiri. Keyword function, waktu dipakai sebagai ekspresi, bisa bikin nilai fungsi. Waktu dipakai sebagai statement, dia bisa dipakai buat mendeklarasikan binding dan ngasih dia fungsi sebagai nilainya. Arrow function adalah cara lain lagi buat bikin fungsi.

// Definisikan f buat megang nilai fungsi
const f = function(a) {
  console.log(a + 2);
};
 
// Deklarasikan g sebagai fungsi
function g(a, b) {
  return a * b * 3.5;
}
 
// Nilai fungsi yang lebih ringkas
let h = a => a % 3;

Bagian kunci dari memahami fungsi adalah memahami scope. Setiap block bikin scope baru. Parameter dan binding yang dideklarasikan di scope tertentu bersifat lokal dan gak terlihat dari luar. Binding yang dideklarasikan dengan var berperilaku beda, mereka berakhir di scope fungsi terdekat atau scope global.

Misahin tugas-tugas yang dilakuin programmu ke fungsi-fungsi yang berbeda itu membantu. Kamu gak perlu terlalu banyak ngulangin diri sendiri, dan fungsi bisa ngebantu ngatur program dengan ngelompokin kode jadi potongan-potongan yang ngelakuin hal tertentu.

Poin-poin pentingnya:

  1. Tiga cara bikin fungsi: ekspresi function, deklarasi function (di-hoist ke atas scope), dan arrow function =>.
  2. let/const itu lokal ke block; var lokal ke fungsi. Scope dalam bisa ngelihat scope luar (lexical scoping), tapi gak sebaliknya.
  3. Fungsi itu nilai biasa: bisa disimpan, dioper, dan ditimpa.
  4. Call stack nyimpen konteks pemanggilan; rekursi terlalu dalam bikin stack overflow.
  5. Argumen kelebihan diabaikan, yang kurang jadi undefined; pakai parameter default = nilai.
  6. Closure: fungsi ngingat environment tempat dia dibikin, bukan tempat dia dipanggil.
  7. Rekursi elegan buat masalah bercabang; jangan optimasi sebelum diukur.
  8. Bikin fungsi dengan nama yang jelas dan satu konsep; fungsi murni lebih gampang dites dan digabung.

Latihan

Minimum

Bab sebelumnya ngenalin fungsi standar Math.min yang ngembaliin argumen terkecilnya. Kita sekarang bisa nulis fungsi kaya gitu sendiri. Definisikan fungsi min yang ngambil dua argumen dan ngembaliin minimumnya.

// Kodemu di sini.
 
console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

Petunjuk:

Kalau kamu kesulitan naruh kurung kurawal dan tanda kurung di tempat yang benar buat dapet definisi fungsi yang valid, mulai dengan nyalin salah satu contoh di bab ini dan modifikasi.

Sebuah fungsi boleh berisi beberapa statement return.

Solusi:

function min(a, b) {
  if (a < b) return a;
  else return b;
}
 
console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

Rekursi

Kita udah lihat kita bisa pakai % (operator sisa bagi) buat ngetes apakah sebuah angka genap atau ganjil dengan pakai % 2 buat lihat apakah dia habis dibagi dua. Ini cara lain buat mendefinisikan apakah bilangan bulat positif genap atau ganjil:

  • Nol itu genap.
  • Satu itu ganjil.
  • Buat angka lain N, kegenapannya sama dengan N - 2.

Definisikan fungsi rekursif isEven yang sesuai dengan deskripsi ini. Fungsinya harus nerima satu parameter (bilangan bulat positif) dan ngembaliin Boolean.

Tes di 50 dan 75. Lihat gimana perilakunya di -1. Kenapa? Bisa kamu pikirin cara memperbaikinya?

// Kodemu di sini.
 
console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??

Petunjuk:

Fungsimu kemungkinan bakal keliatan agak mirip fungsi dalam find di contoh findSolution rekursif di bab ini, dengan rantai if/else if/else yang ngetes mana dari tiga kasus yang berlaku. else terakhir, yang sesuai kasus ketiga, bikin pemanggilan rekursif. Setiap cabang harus berisi statement return atau dengan cara lain ngatur supaya nilai tertentu dikembaliin.

Waktu dikasih angka negatif, fungsinya bakal rekursi lagi dan lagi, ngasih dirinya sendiri angka yang makin negatif, jadi makin jauh dari ngembaliin hasil. Dia akhirnya bakal kehabisan ruang stack dan berhenti.

Solusi:

function isEven(n) {
  if (n == 0) return true;
  else if (n == 1) return false;
  else if (n < 0) return isEven(-n);
  else return isEven(n - 2);
}
 
console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → false

Ngitung Kacang

Kamu bisa dapet karakter, atau huruf, ke-N dari string dengan nulis [N] setelah stringnya (misalnya, string[2]). Nilai hasilnya bakal berupa string yang cuma berisi satu karakter (misalnya, "b"). Karakter pertama punya posisi 0, yang bikin yang terakhir ditemuin di posisi string.length - 1. Dengan kata lain, string dua karakter punya panjang 2, dan karakternya punya posisi 0 dan 1.

Tulis fungsi bernama countBs yang ngambil string sebagai satu-satunya argumen dan ngembaliin angka yang nunjukin berapa banyak karakter B huruf besar yang ada di string itu.

Berikutnya, tulis fungsi bernama countChar yang berperilaku kaya countBs, kecuali dia ngambil argumen kedua yang nunjukin karakter yang mau dihitung (bukannya cuma ngitung karakter B huruf besar). Tulis ulang countBs biar memanfaatkan fungsi baru ini.

// Kodemu di sini.
 
console.log(countBs("BOB"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4

Petunjuk:

Fungsimu bakal butuh loop yang ngelihat setiap karakter di string. Dia bisa ngejalanin indeks dari nol sampai satu di bawah panjangnya (< string.length). Kalau karakter di posisi saat ini sama dengan yang dicari fungsinya, dia nambahin 1 ke variabel counter. Begitu loop-nya selesai, counter-nya bisa dikembaliin.

Hati-hati buat bikin semua binding yang dipakai di fungsi lokal ke fungsinya dengan mendeklarasikannya dengan benar pakai keyword let atau const.

Solusi:

function countChar(string, ch) {
  let counted = 0;
  for (let i = 0; i < string.length; i++) {
    if (string[i] == ch) {
      counted += 1;
    }
  }
  return counted;
}
 
function countBs(string) {
  return countChar(string, "B");
}
 
console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4