1 September 2021

Objek fungsi, NFE

Seperti yang telah kita tahu, sebuah fungsi didalam Javascript adalah sebuah nilai.

Setiap nilai didalam Javascript memiliki sebuah tipe. Apa tipe dari sebuah fungsi?

Didalam Javascript, fungsi adalah objek.

Sebuah cara yang bagus untuk membayangkan fungsi adalah sebagai ???aksi objek??? yang dapat dipanggil. Kita tidak hanya memanggil mereka, tapi juga memperlakukannya seperti sebuah objek: menambah/menghapus properti, memberikan referensi dll.

Properti ???name???

Objek fungsi mengandung beberapa properti yang dapat digunakan.

Contoh, sebuah nama fungsi dapat diakses sebagai properti ???name???:

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

Itu terlihat cukup lucu, logika untuk penggunaan nama cukup pintar. Itu juga menggunakan nama fungsi yang benar bahkan jika tidak terdapat nama sekalipun, dan jika langsung ditempatkan:

let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); // sayHi (ada namanya!)

Itu juga akan bekerja jika assignmentnya dilakukan dengan menggunakan nilai default:

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi (bekerja!)
}

f();

Didalam spesifikasinya, fitur ini dinamakan dengan ???nama kontekstual/contextual name???. Jika sebuah fungsi tidak memiliki nama, maka didalam assignment-nya akan mencarinya didalam konteksnya.

Metode objek mempunyai nama juga:

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

Tidak ada sulap disini. Terdapat kasus dimana tidak ada cara untuk mengetahui namanya. Didalam kasus itu, properti namanya akan kosong, seperti disini:

// fungsi dibuat didalam array
let arr = [function() {}];

alert( arr[0].name ); // <string kosong>
// mesinnya tidak memiliki cara untuk mengetahui namanya, maka isinya akan menjadi kosong

Didalam penerapannya, entah bagaimana, kebanyakan fungsi akan memiliki nama.

Properti ???length???

Juga terdapat properti bawaan ???length??? yang mengembalikan jumlah dari parameter sebuah fungsi, contoh:

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

Disini kita bisa melihat parameter rest tidak dihitung.

Properti length terkadang digunakan untuk introspeksi didalam fungsi yang mengoperasikan fungsi lainnya.

Contoh, didalam kode dibawah fungsi ask menerima sebuah question untuk ditanyakan dan sebuah angka yang panjang dari fungsi handler untuk dipanggil.

Sekalinya pengguna memberikan jawaban mereka, pemanggilan fungsi akan memanggil handler-nya. Kita bisa memberikan dua macan handler:

  • Fungsi dengan jumlah argumen nol, yang mana hanya dipanggil ketika pengguna memberikan jawaban yang positif.
  • Fungsi dengan argumen, yang mana akan dipanggil diantara salah satu kasusnya dan mengembalikan sebuah jawaban.

Untuk memanggil handler dengan cara yang tepat, kita memeriksa properti handler.length.

Idenya adalah kita mempunyai handler yang simpel tanpa argumen untuk kasus yang positif (pada kasus yang sering terjadi), tapi yang tentunya tidak mendukung handler yang universal:

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// untuk jawaban yang positif, kedua handler-nya akan dipanggil
// untuk jawaban yang negatif, hanya yang kedua
ask("Question?", () => alert('You said yes'), result => alert(result));

Ini hanyalah kasus tertentu yang dipanggil dengan polimorfisme ??? memperlakukan argumen berbeda-beda tergantung dari tipenya atau, didalam kasus kita tergantung dari length. Idenya adalah didalam penggunakan librari Javascript.

Properti-properti kustom/Custom properties

Kita juga bisa menambahkan properti-properti milik kita sendiri.

Disini kita menambahkan properti counter untuk melacar total dari pemanggilan:

function sayHi() {
  alert("Hi");

  // hitung berapa kali kita berjalan
  sayHi.counter++;
}
sayHi.counter = 0; // nilai awal

sayHi(); // Hi
sayHi(); // Hi

alert( `Called ${sayHi.counter} times` ); // dipanggil dua kali
Sebuah properti bukanlah sebuah variabel

Sebuah properti dimasukan kedalam fungsi seperti sayHi.counter = 0 tidak mendefinisikan variabel lokal counter didalamnya. Dengan kata lain, sebuah properti counter dan sebuah variabel let counter adalah dua hal yang tidak memiliki hubungam sama sekali.

Kita bisa memperlakukan sebuah fungsi seperti sebuah objek, menyimpan properti didalamnya, tapi itu tidak akan mempengaruhi eksekusinya sendiri. Variabel bukanlah sebuah properti fungsi dan sebaliknya. Keduanya hanyalah dua hal yang berbeda.

Terkadang properti fungsi bisa menggantikan closure. Contoh, kita bisa menulis ulang contoh fungsi counter dari bab Lingkup variabel, closure untuk menggunakan properti fungsi:

function makeCounter() {
  // daripada:
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

countnya sekarang tersimpan didalam fungsinya langsung, bukan diluar dari lingkungan leksikalnya.

Apakah lebih baik atau tidak menggunakan closure?

Perbedaan utamanya adalah jika nilai dari count berada didalam variabel luar, maka kode eksternal tidak dapat mengaksesnya. Hanya fungsi bercabang yang bisa memodifikasinya. Dan jika itu terikat dengan sebuah fungsi, maka kode eksternal dapat mengaksesnya.

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

Jadi pilihannya implementasinya adalah tergantung pada kebutuhan kita.

Ekspresi fungsi yang mempunyai nama

Ekspresi fungsi yang mempunyai nama, atau NFE(Named Function Expression) adalah istilah untuk ekspresi fungsi yang memiliki nama.

Contoh, ayo kita lihat ekspresi fungsi yang biasa:

let sayHi = function(who) {
  alert(`Hello, ${who}`);
};

Dan tambahkan nama:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

Apakah kita meraih sesuatu disini? Apa tujuan dari penambahan nama "func"?

pertama kita perhatikan, bahwa kita masih memiliki ekspresi fungsi. Menambahkan nama "func" setelah function tidaklah membuatnya menjadi deklarasi fungsi, karena itu masih dibuat sebagai bagian dari sebuah assignment ekspresi.

Menambahkan nama seperti itu tidak akan merusak apapun.

Fungsinya masih ada sebagai sayHi():

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

sayHi("John"); // Hello, John

Terdapat dua hal yang spesial tentang nama func, hal itu adalah:

  1. Itu mengijinkan fungsinya untuk mereferensi dirinya sendiri secara internal.
  2. Fungsinya tidak akan terlihat diluar fungsi tersebut.

Untuk contoh, fungsi sayHi dibawah memanggil dirinya-sendiri lagi dengan "Guest" jika who tidak ada:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // gunakan func untuk memanggil dirinya sendiri
  }
};

sayHi(); // Hello, Guest

// Tapi hal ini tidak akan bekerja
func(); // Error, func is not defined (tidak terliha diluar fungsinya sendiri)

Kenapa kita menggunakan func? Mungkin cukup gunakan sayHi untuk pemanggilan bercabang?

Sebenarnya, dalam kebanyakan kasus kita bisa:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest");
  }
};

Masalahnya dengan kode itu adalah sayHi mungkin akan berubah di kode terluar. Malah jika fungsinya dimasukan kedalam variabel lain, kodenya akan mulai memberikan error:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Error, pemanggilan bercabang sayHi tidak akan bekerja lagi!

Hal itu terjadi karena fungsinya menggunakan sayHi dari luar lingkungan leksikalnya. Disana tidak ada sayHi lokal, jadi variabel terluar digunakan. Dan pada saat pemanggilannya terjadi sayHi terluar adalah null.

Nama opsional dimana kita bisa memasukannya kedalam ekspresi fungsi diartikan untuk menyelesaikan masalah yang tepat seperti ini.

Ayo kita gunakan itu untuk membetulkan masalah pada kode kita:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // Sekarang semuanya mantap
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (pemanggilan bercabang bekerja)

Sekarang hal itu bekerja karena nama "func" adalah fungsi-lokal. Fungsi itu tidak diambil dari luar (dan tidak terlihat dari luar). Spesifikasinya menjamin itu akan selalu mereferensi fungsi saat ini.

Fungsi dari luar kode mempunyai variabel sayHi atau welcomenya sendiri. Dan func adalah sebuah ???nama fungsi internal???, bagaimana fungsi bisa memanggil dirinya sendiri secara internal.

Tidak ada hal semacam itu untuk deklarasi fungsi

Fitur ???nama internal??? dideskripsikan disini hanya tersedia untuk ekspresi fungsi, bukan deklarasi fungsi. Untuk deklarasi fungsi, tidak terdapat sintaks untuk menambahkan sebuah nama ???internal???.

Terkadang, ketika kita membutuhkan nama internal yang dapat diandalkan, itulah alasan yang tepat untuk menulis ulang sebuah deklarasi fungsi kedalam bentuk ekspresi fungsi.

Ringkasan

Fungsi adalah objek.

Disini kita memperlajari properti-propertinya:

  • name ??? nama dari fungsinya. Biasanya diambil dari definisi fungsinya, tapi jika disana tidak ada, Javascript akan mencoba mencarinya dari konteksnya (contoh. dari assignment-nya).
  • length ??? jumlah dari argumen didalam definisi dari fungsi. Parameter rest tidak dihitung.

Jika fungsinya di deklarasikan sebagai ekspresi fungsi (tidak didalam alur kode utama), dan itu memiliki nama, maka itu dipanggil dengan ekspresi fungsi yang memiliki nama. Namanya bisa digunakan didalam fungsinya untuk mereferensi dirinya sendiri, untuk pemanggilan rekursif atau sejenisnya.

Juga, fungsi mungkin memiliki properti tambahan. Beberapa librari Javascript yang cukup terkenal banyak menggunakan fitur ini.

Mereka membuat sebuah fungsi ???utama??? dan mengkaitkannya dengan fungsi ???pembantu???. Contoh librari jQuery menciptakan fungsi bernama $. Librari The lodash membuat sebuah fungsi _ dan lalu menambahkan _.clone, _.keyBydan properti lainnya kedalamnya (lihat dokumentasinya ketika kamu mau tau lebih dalam). Sebenarnya, mereka melakukannya untuk mengurangi penggunaan dari ruang global, jadi librari tunggal itu hanya menggunakan satu variabel global. Itu mengurangi kemungkinan dari konflik penamaan variabel.

Jadi, sebuah fungsi bisa melakukan hal-hal yang berguna dengan dirinya-sendiri dan juga membawa setumpuk fungsionalitas didalam propertinya sendiri.

Tugas

Modifikasi kode dari makeCounter() jadi penghitungnya juga bisa mengurangi dan menyetel ulang angkanya:

  • counter() harus mengembalikan angka selanjutnya (seperti sebelumnya).
  • counter.set(value) harus menyetel ulang penghitungnya jadi value.
  • counter.decrease() harus mengurangi angka penghitungnya dengan 1.

Lihat kode pada sandbox untuk contoh penggunaan yang lengkap.

Catatan. Kamu bisa menggunakan closure atau properti fungsi untuk menyimpan perhitungan yang sekarang. Atau tulis kedua variannya.

Buka sandbox dengan tes.

Solusinya adalah menggunakan count didalam variabel lokal, tapi metode tambahan ditulis tepat didalam counternya. Mereka membagi lingkungan leksikan luar yang sama dan juga bisa mengakses count yang sekarang.

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

Buka solusi dengan tes di sandbox.

Buatlah sebuah fungsi sum yang harus bekerja seperti ini:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

Catatan. Kamu mungkin perlu untuk mengatur objek kostum menjadi perngubah primitif didalam fungsi kamu.

Buka sandbox dengan tes.

  1. Untuk membuat semuanya bekerja entah bagaimana, hasil dari sum haruslah sebuah fungsi.
  2. Fungsi harus menyimpan nilai sekarang yang berada diantara pemanggilan didalam memori.
  3. Menurut tasknya, fungsinya harus menjadi angka ketika digunakan didalam ==. Fungsi adalah objek, jadi perubahan terjasi seperti yang dideskripsikan didalam bab Menolak konversi primitif, dan kita bisa menyediakan metode milik kita yang mengembalikan angkanya.

Sekarang angkanya:

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

Perhatikan baik-baik bahwa fungsi sum sebenarnya hanya bekerja satu kali. Itu mengembalikan fungsi f.

Lalu, untuk setiap pemanggilan selanjutnya f menambahkan parameternya kedlaam currentSum, dan mengembalikan dirinya sendiri.

Tidak terdapat rekursi di akhir baris dari f.

Ini adalah bagaimana rekursi terlihat:

function f(b) {
  currentSum += b;
  return f(); // <-- pemanggilan rekursi
}

Dan didalam kasus kita, kita hanya mengembalikan fungsinya, tanpa memanggilnya:

function f(b) {
  currentSum += b;
  return f; // <-- tidak memanggil dirinya-sendiri, hanya mengembalikan dirinya
}

f ini akan digunakan didalam pemanggilan selanjutnya, dan lagi akan mengembalikan dirinya-sendiri, berapa kalipun seperti yang dibutuhkan. Lalu, ketika digunakan sebagai angka atau sebuah string ??? toString mengembalikan currentSum. Kita jadi bisa menggunakan Symbol.toPrimitive atau valueOf disini sebagai perubahan.

Buka solusi dengan tes di sandbox.

Peta tutorial