Bab 6 dari Eloquent JavaScript. Sebelumnya: 05 Fungsi Tingkat Tinggi. Berikutnya: 07 Proyek Robot Pengantar Paket.
Abstract data type diwujudkan dengan nulis jenis program khusus […] yang mendefinisikan tipenya dalam bentuk operasi-operasi yang bisa dilakuin padanya.
— Barbara Liskov, Programming with Abstract Data Types

Bab 4 ngenalin objek JavaScript sebagai wadah yang megang data lain. Di budaya pemrograman, object-oriented programming (pemrograman berorientasi objek) adalah sekumpulan teknik yang pakai objek sebagai prinsip sentral pengorganisasian program. Walaupun gak ada yang beneran sepakat soal definisi persisnya, pemrograman berorientasi objek udah membentuk desain banyak bahasa pemrograman, termasuk JavaScript. Bab ini ngejelasin cara ide-ide ini bisa diterapin di JavaScript.
Abstract Data Type
Ide utama di pemrograman berorientasi objek adalah pakai objek, atau lebih tepatnya tipe objek, sebagai unit pengorganisasian program. Nyusun program sebagai sejumlah tipe objek yang terpisah ketat nyediain cara buat mikirin strukturnya dan dengan begitu menegakkan semacam disiplin, nyegah semuanya jadi kusut.
Caranya adalah ngebayangin objek agak kaya kamu ngebayangin mixer listrik atau peralatan rumah tangga lainnya. Orang-orang yang ngedesain dan ngerakit mixer harus ngelakuin kerjaan khusus yang butuh ilmu material dan pemahaman kelistrikan. Mereka nutup semua itu dalam cangkang plastik yang mulus jadi orang-orang yang cuma mau ngaduk adonan panekuk gak perlu khawatir soal semua itu, mereka cuma perlu paham beberapa tombol yang bisa dipakai buat ngoperasiin mixer-nya.
Sama juga, abstract data type, atau object class, adalah subprogram yang mungkin berisi kode serumit apa pun tapi cuma mengekspos sekumpulan terbatas method dan properti yang seharusnya dipakai orang-orang yang kerja dengannya. Ini ngebolehin program besar dibangun dari sejumlah tipe peralatan, ngebatasin sejauh mana bagian-bagian berbeda ini kusut dengan mengharuskan mereka cuma berinteraksi satu sama lain dengan cara tertentu.
Kalau masalah ditemuin di salah satu object class kaya gitu, dia sering bisa diperbaiki atau bahkan ditulis ulang sepenuhnya tanpa ngaruh ke sisa programnya. Lebih bagus lagi, mungkin aja pakai object class di beberapa program berbeda, ngehindarin kebutuhan bikin ulang fungsionalitasnya dari nol. Kamu bisa ngebayangin struktur data bawaan JavaScript, kaya array dan string, sebagai abstract data type yang bisa dipakai ulang kaya gitu.
Setiap abstract data type punya interface (antarmuka), kumpulan operasi yang bisa dilakuin kode eksternal padanya. Detail apa pun di luar interface itu dienkapsulasi, diperlakukan sebagai internal ke tipenya dan gak jadi urusan sisa programnya.
Bahkan hal dasar kaya angka bisa dibayangin sebagai abstract data type yang interface-nya ngebolehin kita nambahinnya, ngaliinnya, ngebandinginnya, dan seterusnya. Sebenernya, fiksasi pada objek tunggal sebagai unit utama pengorganisasian di pemrograman berorientasi objek klasik agak disayangkan karena potongan fungsionalitas yang berguna sering melibatkan sekelompok object class berbeda yang kerja bareng dengan erat.
Method
Di JavaScript, method gak lebih dari properti yang megang nilai fungsi. Ini method sederhana:
function speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
let whiteRabbit = {type: "white", speak};
let hungryRabbit = {type: "hungry", speak};
whiteRabbit.speak("Oh my fur and whiskers");
// → The white rabbit says 'Oh my fur and whiskers'
hungryRabbit.speak("Got any carrots?");
// → The hungry rabbit says 'Got any carrots?'Biasanya method perlu ngelakuin sesuatu dengan objek tempat dia dipanggil. Waktu fungsi dipanggil sebagai method, dicari sebagai properti dan langsung dipanggil, kaya di object.method(), binding bernama this di badannya otomatis nunjuk ke objek tempat dia dipanggil.
Kamu bisa ngebayangin this sebagai parameter ekstra yang dikasih ke fungsi dengan cara berbeda dari parameter biasa. Kalau kamu mau nyediainnya secara eksplisit, kamu bisa pakai method call dari fungsi, yang ngambil nilai this sebagai argumen pertamanya dan memperlakukan argumen selanjutnya sebagai parameter normal.
speak.call(whiteRabbit, "Hurry");
// → The white rabbit says 'Hurry'Karena setiap fungsi punya binding this-nya sendiri yang nilainya tergantung cara dia dipanggil, kamu gak bisa ngerujuk ke this dari scope pembungkus di fungsi biasa yang didefinisikan dengan keyword function.
Arrow function beda, mereka gak ngikat this mereka sendiri tapi bisa ngelihat binding this dari scope di sekitarnya. Jadi, kamu bisa ngelakuin sesuatu kaya kode berikut, yang ngerujuk this dari dalam fungsi lokal:
let finder = {
find(array) {
return array.some(v => v == this.value);
},
value: 5
};
console.log(finder.find([4, 5]));
// → trueProperti kaya find(array) di ekspresi objek adalah cara singkat buat mendefinisikan method. Dia bikin properti bernama find dan ngasih dia fungsi sebagai nilainya.
Kalau saya nulis argumen ke some pakai keyword function, kode ini gak bakal jalan.
❌ salah, function biasa punya this sendiri, jadi this.value jadi undefined
let finder = {
find(array) {
return array.some(function(v) { return v == this.value; });
},
value: 5
};
console.log(finder.find([4, 5]));
// → false✅ benar, arrow function minjem this dari method find
let finder = {
find(array) {
return array.some(v => v == this.value);
},
value: 5
};
console.log(finder.find([4, 5]));
// → truePrototype
Salah satu cara bikin tipe objek kelinci dengan method speak adalah bikin fungsi pembantu yang punya tipe kelinci sebagai parameternya dan ngembaliin objek yang megang itu sebagai properti type-nya dan fungsi speak kita di properti speak-nya.
Semua kelinci berbagi method yang sama itu. Terutama buat tipe dengan banyak method, bakal bagus kalau ada cara buat nyimpen method-method sebuah tipe di satu tempat, bukannya nambahinnya ke setiap objek satu per satu.
Di JavaScript, prototype adalah cara buat ngelakuin itu. Objek bisa dihubungin ke objek lain, buat secara ajaib dapet semua properti yang dipunya objek lain itu. Objek biasa yang dibikin dengan notasi {} terhubung ke objek bernama Object.prototype.
let empty = {};
console.log(empty.toString);
// → function toString(){…}
console.log(empty.toString());
// → [object Object]Keliatannya kita baru aja narik properti dari objek kosong. Tapi sebenernya, toString adalah method yang disimpan di Object.prototype, artinya dia tersedia di kebanyakan objek.
Waktu objek dapet permintaan buat properti yang gak dia punya, prototype-nya bakal dicari buat properti itu. Kalau itu gak punya, prototype dari prototype-nya yang dicari, dan seterusnya sampai nyampe objek tanpa prototype (Object.prototype adalah objek kaya gitu).
console.log(Object.getPrototypeOf({}) == Object.prototype);
// → true
console.log(Object.getPrototypeOf(Object.prototype));
// → nullKaya yang kamu tebak, Object.getPrototypeOf ngembaliin prototype dari sebuah objek.
Banyak objek gak langsung punya Object.prototype sebagai prototype-nya tapi punya objek lain yang nyediain kumpulan properti default yang berbeda. Fungsi diturunin dari Function.prototype dan array diturunin dari Array.prototype.
console.log(Object.getPrototypeOf(Math.max) ==
Function.prototype);
// → true
console.log(Object.getPrototypeOf([]) == Array.prototype);
// → trueObjek prototype kaya gitu sendiri bakal punya prototype, sering kali Object.prototype, jadi dia masih secara gak langsung nyediain method kaya toString.
Kamu bisa pakai Object.create buat bikin objek dengan prototype tertentu.
let protoRabbit = {
speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
};
let blackRabbit = Object.create(protoRabbit);
blackRabbit.type = "black";
blackRabbit.speak("I am fear and darkness");
// → The black rabbit says 'I am fear and darkness'Kelinci “proto” bertindak sebagai wadah buat properti yang dibagi semua kelinci. Objek kelinci individual, kaya kelinci hitam, berisi properti yang cuma berlaku buat dirinya sendiri, di kasus ini type-nya, dan nurunin properti bersama dari prototype-nya.
Class
Sistem prototype JavaScript bisa ditafsirkan sebagai versi yang agak bebas dari abstract data type atau class. Class mendefinisikan bentuk dari sebuah tipe objek, method dan properti apa yang dia punya. Objek kaya gitu disebut instance dari class-nya.
Prototype berguna buat mendefinisikan properti yang semua instance dari sebuah class berbagi nilai yang sama. Properti yang beda per instance, kaya properti type kelinci kita, perlu disimpan langsung di objeknya sendiri.
Buat bikin instance dari class tertentu, kamu harus bikin objek yang diturunin dari prototype yang tepat, tapi kamu juga harus mastiin dia sendiri punya properti yang seharusnya dipunya instance class ini. Inilah yang dilakuin fungsi constructor.
function makeRabbit(type) {
let rabbit = Object.create(protoRabbit);
rabbit.type = type;
return rabbit;
}Notasi class JavaScript bikin lebih gampang mendefinisikan fungsi jenis ini, bareng objek prototype.
class Rabbit {
constructor(type) {
this.type = type;
}
speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
}Keyword class memulai deklarasi class, yang ngebolehin kita mendefinisikan constructor dan sekumpulan method bareng-bareng. Berapa pun method boleh ditulis di dalam kurung kurawal deklarasinya. Kode ini punya efek mendefinisikan binding bernama Rabbit, yang megang fungsi yang ngejalanin kode di constructor dan punya properti prototype yang megang method speak.
Fungsi ini gak bisa dipanggil kaya fungsi normal. Constructor, di JavaScript, dipanggil dengan naruh keyword new di depannya. Ngelakuin itu bikin objek instance baru yang prototype-nya adalah objek dari properti prototype fungsinya, lalu ngejalanin fungsinya dengan this terikat ke objek baru, dan terakhir ngembaliin objeknya.
let killerRabbit = new Rabbit("killer");Sebenernya, class baru diperkenalkan di edisi 2015 JavaScript. Fungsi apa pun bisa dipakai sebagai constructor, dan sebelum 2015, cara mendefinisikan class adalah nulis fungsi biasa lalu memanipulasi properti prototype-nya.
function ArchaicRabbit(type) {
this.type = type;
}
ArchaicRabbit.prototype.speak = function(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
};
let oldSchoolRabbit = new ArchaicRabbit("old school");Karena alasan ini, semua fungsi non-arrow dimulai dengan properti prototype yang megang objek kosong.
Berdasarkan konvensi, nama constructor dikapitalisasi biar gampang dibedain dari fungsi lain.
Penting buat paham perbedaan antara cara prototype dikaitin dengan constructor (lewat properti prototype-nya) dan cara objek punya prototype (yang bisa ditemuin dengan Object.getPrototypeOf). Prototype sebenarnya dari sebuah constructor adalah Function.prototype karena constructor itu fungsi. Properti prototype fungsi constructor megang prototype yang dipakai buat instance yang dibikin lewat dia.
console.log(Object.getPrototypeOf(Rabbit) ==
Function.prototype);
// → true
console.log(Object.getPrototypeOf(killerRabbit) ==
Rabbit.prototype);
// → trueConstructor biasanya bakal nambahin beberapa properti per-instance ke this. Bisa juga mendeklarasikan properti langsung di deklarasi class. Beda dengan method, properti kaya gitu ditambahin ke objek instance dan bukan ke prototype.
class Particle {
speed = 0;
constructor(position) {
this.position = position;
}
}Kaya function, class bisa dipakai di statement maupun di ekspresi. Waktu dipakai sebagai ekspresi, dia gak mendefinisikan binding tapi cuma ngehasilin constructor sebagai nilai. Kamu boleh ngilangin nama class di ekspresi class.
let object = new class { getWord() { return "hello"; } };
console.log(object.getWord());
// → helloProperti Privat
Umum buat class mendefinisikan beberapa properti dan method buat penggunaan internal yang bukan bagian dari interface-nya. Ini disebut properti privat, lawan dari yang publik, yang jadi bagian dari interface eksternal objeknya.
Buat mendeklarasikan method privat, taruh tanda # di depan namanya. Method kaya gitu cuma bisa dipanggil dari dalam deklarasi class yang mendefinisikannya.
class SecretiveObject {
#getSecret() {
return "I ate all the plums";
}
interrogate() {
let shallISayIt = this.#getSecret();
return "never";
}
}Waktu class gak mendeklarasikan constructor, dia otomatis dapet yang kosong.
Kalau kamu nyoba manggil #getSecret dari luar class, kamu dapet error. Keberadaannya sepenuhnya tersembunyi di dalam deklarasi class.
Buat pakai properti instance privat, kamu harus mendeklarasikannya. Properti biasa bisa dibikin cuma dengan nge-assign ke mereka, tapi properti privat harus dideklarasikan di deklarasi class biar tersedia sama sekali.
Class ini ngimplementasiin peralatan buat dapet bilangan bulat acak di bawah angka maksimum tertentu. Dia cuma punya satu properti publik: getNumber.
class RandomSource {
#max;
constructor(max) {
this.#max = max;
}
getNumber() {
return Math.floor(Math.random() * this.#max);
}
}Menimpa Properti Turunan
Waktu kamu nambahin properti ke objek, entah dia ada di prototype atau gak, propertinya ditambahin ke objeknya sendiri. Kalau udah ada properti dengan nama yang sama di prototype, properti ini gak bakal ngaruh lagi ke objeknya, karena dia sekarang tersembunyi di balik properti milik objek sendiri.
Rabbit.prototype.teeth = "small";
console.log(killerRabbit.teeth);
// → small
killerRabbit.teeth = "long, sharp, and bloody";
console.log(killerRabbit.teeth);
// → long, sharp, and bloody
console.log((new Rabbit("basic")).teeth);
// → small
console.log(Rabbit.prototype.teeth);
// → smallDiagram berikut nyeket situasinya setelah kode ini jalan. Prototype Rabbit dan Object ada di belakang killerRabbit sebagai semacam latar, tempat properti yang gak ditemuin di objeknya sendiri bisa dicari.
Nimpa properti yang ada di prototype bisa jadi hal yang berguna. Kaya yang ditunjukin contoh gigi kelinci, penimpaan bisa dipakai buat ngekspresiin properti yang luar biasa di instance dari class objek yang lebih generik sambil ngebiarin objek yang gak luar biasa ngambil nilai standar dari prototype-nya.
Penimpaan juga dipakai buat ngasih prototype fungsi dan array standar method toString yang berbeda dari prototype objek dasar.
console.log(Array.prototype.toString ==
Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2Manggil toString di array ngasih hasil yang mirip manggil .join(",") di dia, dia naruh koma di antara nilai-nilai di array. Langsung manggil Object.prototype.toString dengan array ngehasilin string yang berbeda. Fungsi itu gak tahu soal array, jadi dia cuma naruh kata object dan nama tipenya di antara kurung siku.
console.log(Object.prototype.toString.call([1, 2]));
// → [object Array]Map
Kita lihat kata map dipakai di bab sebelumnya buat operasi yang mentransformasi struktur data dengan nerapin fungsi ke elemen-elemennya. Membingungkan emang, di pemrograman kata yang sama dipakai buat hal yang berhubungan tapi agak beda.
Map (kata benda) adalah struktur data yang mengaitkan nilai (kunci) dengan nilai lain. Misalnya, kamu mungkin mau memetakan nama ke umur. Bisa aja pakai objek buat ini.
let ages = {
Boris: 39,
Liang: 22,
Júlia: 62
};
console.log(`Júlia is ${ages["Júlia"]}`);
// → Júlia is 62
console.log("Is Jack's age known?", "Jack" in ages);
// → Is Jack's age known? false
console.log("Is toString's age known?", "toString" in ages);
// → Is toString's age known? trueDi sini, nama properti objeknya adalah nama orang dan nilai propertinya adalah umur mereka. Tapi kita pasti gak nyantumin siapa pun bernama toString di map kita. Tapi karena objek biasa diturunin dari Object.prototype, keliatannya propertinya ada.
Karena alasan ini, pakai objek biasa sebagai map itu berbahaya. Ada beberapa cara yang mungkin buat ngehindarin masalah ini. Pertama, kamu bisa bikin objek tanpa prototype. Kalau kamu ngasih null ke Object.create, objek hasilnya gak bakal diturunin dari Object.prototype dan bisa dipakai dengan aman sebagai map.
console.log("toString" in Object.create(null));
// → falseNama properti objek harus string. Kalau kamu butuh map yang kuncinya gak gampang dikonversi ke string, kaya objek, kamu gak bisa pakai objek sebagai map-mu.
Untungnya, JavaScript datang dengan class bernama Map yang ditulis buat tujuan persis ini. Dia nyimpen pemetaan dan ngebolehin tipe kunci apa pun.
let ages = new Map();
ages.set("Boris", 39);
ages.set("Liang", 22);
ages.set("Júlia", 62);
console.log(`Júlia is ${ages.get("Júlia")}`);
// → Júlia is 62
console.log("Is Jack's age known?", ages.has("Jack"));
// → Is Jack's age known? false
console.log(ages.has("toString"));
// → falseMethod set, get, dan has adalah bagian dari interface objek Map. Nulis struktur data yang bisa cepat ngupdate dan nyari sekumpulan besar nilai itu gak gampang, tapi kita gak perlu khawatir soal itu. Orang lain udah ngerjainnya buat kita, dan kita bisa lewat interface sederhana ini buat pakai hasil kerja mereka.
Kalau kamu emang punya objek biasa yang perlu kamu perlakukan sebagai map karena suatu alasan, berguna buat tahu bahwa Object.keys cuma ngembaliin kunci milik objek sendiri, bukan yang di prototype. Sebagai alternatif operator in, kamu bisa pakai fungsi Object.hasOwn, yang ngabaikan prototype objeknya.
console.log(Object.hasOwn({x: 1}, "x"));
// → true
console.log(Object.hasOwn({x: 1}, "toString"));
// → falsePolymorphism
Waktu kamu manggil fungsi String (yang ngonversi nilai ke string) di sebuah objek, dia bakal manggil method toString di objek itu buat nyoba bikin string yang bermakna darinya. Saya nyebutin bahwa beberapa prototype standar mendefinisikan versi toString mereka sendiri biar mereka bisa bikin string yang berisi informasi lebih berguna dari "[object Object]". Kamu juga bisa ngelakuin itu sendiri.
Rabbit.prototype.toString = function() {
return `a ${this.type} rabbit`;
};
console.log(String(killerRabbit));
// → a killer rabbitIni instance sederhana dari ide yang kuat. Waktu sepotong kode ditulis buat kerja dengan objek yang punya interface tertentu, di kasus ini, method toString, jenis objek apa pun yang kebetulan mendukung interface ini bisa dicolokin ke kodenya dan bakal bisa kerja dengannya.
Teknik ini disebut polymorphism. Kode polimorfik bisa kerja dengan nilai berbagai bentuk, selama mereka mendukung interface yang dia harapkan.
Contoh interface yang dipakai luas adalah objek mirip-array yang punya properti length yang megang angka dan properti bernomor buat setiap elemennya. Array maupun string mendukung interface ini, begitu juga berbagai objek lain, beberapa di antaranya bakal kita lihat nanti di bab-bab tentang browser. Kamu bisa manggil kebanyakan method array, kaya Array.prototype.forEach, di apa pun yang nyediain interface ini.
Array.prototype.forEach.call({
length: 2,
0: "A",
1: "B"
}, elt => console.log(elt));
// → A
// → BGetter, Setter, dan Static
Interface sering berisi properti biasa, bukan cuma method. Misalnya, objek Map punya properti size yang ngasih tahu kamu berapa banyak kunci yang disimpan di dalamnya.
Gak perlu buat objek kaya gitu ngitung dan nyimpen properti itu langsung di instance. Bahkan properti yang diakses langsung mungkin nyembunyiin pemanggilan method. Method kaya gitu disebut getter dan didefinisikan dengan nulis get di depan nama method di ekspresi objek atau deklarasi class.
let varyingSize = {
get size() {
return Math.floor(Math.random() * 100);
}
};
console.log(varyingSize.size);
// → 73
console.log(varyingSize.size);
// → 49Setiap kali seseorang baca dari properti size objek ini, method terkaitnya dipanggil. Kamu bisa ngelakuin hal serupa waktu properti ditulis, pakai setter.
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 1.8 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) / 1.8;
}
static fromFahrenheit(value) {
return new Temperature((value - 32) / 1.8);
}
}
let temp = new Temperature(22);
console.log(temp.fahrenheit);
// → 71.6
temp.fahrenheit = 86;
console.log(temp.celsius);
// → 30Class Temperature ngebolehin kamu baca dan nulis suhu dalam derajat Celsius atau derajat Fahrenheit, tapi secara internal dia cuma nyimpen Celsius dan otomatis ngonversi ke dan dari Celsius di getter dan setter fahrenheit.
Kadang kamu mau nempelin beberapa properti langsung ke fungsi constructor-mu bukannya ke prototype. Method kaya gitu gak bakal punya akses ke instance class tapi bisa, misalnya, dipakai buat nyediain cara tambahan bikin instance.
Di dalam deklarasi class, method atau properti yang punya static ditulis sebelum namanya disimpan di constructor. Misalnya, class Temperature ngebolehin kamu nulis Temperature.fromFahrenheit(100) buat bikin suhu pakai derajat Fahrenheit.
let boil = Temperature.fromFahrenheit(212);
console.log(boil.celsius);
// → 100Symbol
Saya nyebutin di Bab 4 bahwa loop for/of bisa ngeloop beberapa jenis struktur data. Ini kasus lain dari polymorphism, loop kaya gitu ngarepin struktur datanya mengekspos interface tertentu, yang dilakuin array dan string. Dan kita juga bisa nambahin interface ini ke objek kita sendiri! Tapi sebelum kita bisa ngelakuin itu, kita perlu sebentar ngelihat tipe symbol.
Bisa aja beberapa interface pakai nama properti yang sama buat hal yang berbeda. Misalnya, di objek mirip-array, length ngerujuk ke jumlah elemen di koleksi. Tapi interface objek yang menggambarkan rute pendakian bisa pakai length buat nyediain panjang rute dalam meter. Gak mungkin buat objek memenuhi kedua interface ini.
Objek yang nyoba jadi rute sekaligus mirip-array (mungkin buat ngenumerasi titik-titik jalurnya) agak mengada-ada, dan masalah jenis ini gak terlalu umum dalam praktik. Tapi buat hal kaya protokol iterasi, perancang bahasa butuh jenis properti yang beneran gak bentrok sama yang lain. Jadi di 2015, symbol ditambahin ke bahasa ini.
Kebanyakan properti, termasuk semua yang udah kita lihat sejauh ini, dinamai dengan string. Tapi bisa juga pakai symbol sebagai nama properti. Symbol adalah nilai yang dibikin dengan fungsi Symbol. Beda dengan string, symbol yang baru dibikin itu unik, kamu gak bisa bikin symbol yang sama dua kali.
let sym = Symbol("name");
console.log(sym == Symbol("name"));
// → false
Rabbit.prototype[sym] = 55;
console.log(killerRabbit[sym]);
// → 55String yang kamu kasih ke Symbol disertain waktu kamu ngonversinya ke string dan bisa bikin lebih gampang ngenalin symbol waktu, misalnya, nampilinnya di konsol. Tapi dia gak punya makna lebih dari itu, beberapa symbol boleh punya nama yang sama.
Jadi unik sekaligus bisa dipakai sebagai nama properti bikin symbol cocok buat mendefinisikan interface yang bisa hidup damai berdampingan dengan properti lain, apa pun namanya.
const length = Symbol("length");
Array.prototype[length] = 0;
console.log([1, 2].length);
// → 2
console.log([1, 2][length]);
// → 0Bisa aja nyertain properti symbol di ekspresi objek dan class dengan pakai kurung siku di sekitar nama propertinya. Itu bikin ekspresi di antara kurungnya dievaluasi buat ngehasilin nama properti, analog dengan notasi akses properti kurung siku.
let myTrip = {
length: 2,
0: "Lankwitz",
1: "Babelsberg",
[length]: 21500
};
console.log(myTrip[length], myTrip.length);
// → 21500 2Interface Iterator
Objek yang dikasih ke loop for/of diharapkan iterable. Ini berarti dia punya method yang dinamai dengan symbol Symbol.iterator (nilai symbol yang didefinisikan bahasanya, disimpan sebagai properti dari fungsi Symbol).
Waktu dipanggil, method itu harus ngembaliin objek yang nyediain interface kedua, iterator. Ini hal sebenarnya yang ngelakuin iterasi. Dia punya method next yang ngembaliin hasil berikutnya. Hasil itu harus berupa objek dengan properti value yang nyediain nilai berikutnya, kalau ada, dan properti done, yang harus true waktu gak ada hasil lagi dan false kalau sebaliknya.
Perhatiin bahwa nama properti next, value, dan done adalah string biasa, bukan symbol. Cuma Symbol.iterator, yang kemungkinan ditambahin ke banyak objek berbeda, yang beneran symbol.
Kita bisa langsung pakai interface ini sendiri.
let okIterator = "OK"[Symbol.iterator]();
console.log(okIterator.next());
// → {value: "O", done: false}
console.log(okIterator.next());
// → {value: "K", done: false}
console.log(okIterator.next());
// → {value: undefined, done: true}Kita implementasiin struktur data iterable yang mirip linked list dari latihan di Bab 4. Kali ini kita bakal nulis list-nya sebagai class.
class List {
constructor(value, rest) {
this.value = value;
this.rest = rest;
}
get length() {
return 1 + (this.rest ? this.rest.length : 0);
}
static fromArray(array) {
let result = null;
for (let i = array.length - 1; i >= 0; i--) {
result = new this(array[i], result);
}
return result;
}
}Perhatiin bahwa this, di method static, nunjuk ke constructor class-nya, bukan instance, gak ada instance di sekitar waktu method static dipanggil.
Iterasi list harus ngembaliin semua elemen list dari awal sampai akhir. Kita bakal nulis class terpisah buat iterator-nya.
class ListIterator {
constructor(list) {
this.list = list;
}
next() {
if (this.list == null) {
return {done: true};
}
let value = this.list.value;
this.list = this.list.rest;
return {value, done: false};
}
}Class-nya ngelacak kemajuan iterasi lewat list dengan ngupdate properti list-nya buat pindah ke objek list berikutnya setiap kali nilai dikembaliin dan ngelaporin bahwa dia selesai waktu list itu kosong (null).
Kita siapin class List biar iterable. Sepanjang buku ini, saya sesekali bakal pakai manipulasi prototype setelah-fakta buat nambahin method ke class biar potongan-potongan kode individualnya tetap kecil dan berdiri sendiri. Di program biasa, di mana gak perlu mecah kode jadi potongan kecil, kamu bakal mendeklarasikan method-method ini langsung di class-nya.
List.prototype[Symbol.iterator] = function() {
return new ListIterator(this);
};Sekarang kita bisa ngeloop list dengan for/of.
let list = List.fromArray([1, 2, 3]);
for (let element of list) {
console.log(element);
}
// → 1
// → 2
// → 3Sintaks ... di notasi array dan pemanggilan fungsi juga jalan dengan objek iterable apa pun. Misalnya, kamu bisa pakai [...value] buat bikin array yang berisi elemen-elemen di objek iterable apa pun.
console.log([..."PCI"]);
// → ["P", "C", "I"]Inheritance
Bayangin kita butuh tipe list yang sangat mirip class List yang kita lihat sebelumnya, tapi karena kita bakal nanya panjangnya terus-terusan, kita gak mau dia harus nyisir rest-nya setiap kali. Sebagai gantinya, kita mau nyimpen panjangnya di setiap instance biar aksesnya efisien.
Sistem prototype JavaScript bikin mungkin buat bikin class baru, sangat mirip class lama, tapi dengan definisi baru buat beberapa propertinya. Prototype buat class baru diturunin dari prototype lama tapi nambahin definisi baru buat, misalnya, getter length.
Dalam istilah pemrograman berorientasi objek, ini disebut inheritance (pewarisan). Class baru mewarisi properti dan perilaku dari class lama.
class LengthList extends List {
#length;
constructor(value, rest) {
super(value, rest);
this.#length = super.length;
}
get length() {
return this.#length;
}
}
console.log(LengthList.fromArray([1, 2, 3]).length);
// → 3Penggunaan kata extends nunjukin bahwa class ini gak boleh langsung didasarkan pada prototype Object default tapi pada class lain. Ini disebut superclass. Class turunannya adalah subclass.
Buat nginisialisasi instance LengthList, constructor-nya manggil constructor superclass-nya lewat keyword super. Ini perlu karena kalau objek baru ini mau berperilaku (kurang lebih) kaya List, dia bakal butuh properti instance yang dipunya list.
Constructor-nya lalu nyimpen panjang list di properti privat. Kalau kita nulis this.length di situ, getter milik class sendiri yang bakal dipanggil, yang belum jalan karena #length belum diisi. Kita bisa pakai super.something buat manggil method dan getter di prototype superclass, yang sering berguna.
Inheritance ngebolehin kita ngebangun tipe data yang sedikit beda dari tipe data yang udah ada dengan kerjaan yang relatif sedikit. Ini bagian fundamental dari tradisi berorientasi objek, bareng enkapsulasi dan polymorphism. Tapi sementara dua yang terakhir sekarang umumnya dianggap ide yang luar biasa, inheritance lebih kontroversial.
Sementara enkapsulasi dan polymorphism bisa dipakai buat misahin potongan kode satu sama lain, ngurangin kekusutan program secara keseluruhan, inheritance secara fundamental ngiket class-class jadi satu, bikin lebih banyak kekusutan. Waktu mewarisi dari class, kamu biasanya harus tahu lebih banyak soal cara kerjanya dibanding waktu cuma pakainya. Inheritance bisa jadi alat yang berguna buat bikin beberapa jenis program lebih ringkas, tapi dia gak seharusnya jadi alat pertama yang kamu raih, dan kamu mungkin gak seharusnya aktif nyari kesempatan buat nyusun hierarki class (pohon keluarga class).
Operator instanceof
Kadang berguna buat tahu apakah sebuah objek diturunin dari class tertentu. Buat ini, JavaScript nyediain operator binary bernama instanceof.
console.log(
new LengthList(1, null) instanceof LengthList);
// → true
console.log(new LengthList(2, null) instanceof List);
// → true
console.log(new List(3, null) instanceof LengthList);
// → false
console.log([1] instanceof Array);
// → trueOperatornya bakal nembus tipe turunan, jadi LengthList adalah instance dari List. Operatornya juga bisa diterapin ke constructor standar kaya Array. Hampir setiap objek adalah instance dari Object.
Ringkasan
Objek ngelakuin lebih dari sekadar megang propertinya sendiri. Mereka punya prototype, yaitu objek lain. Mereka bakal bertindak seolah punya properti yang gak mereka punya selama prototype-nya punya properti itu. Objek sederhana punya Object.prototype sebagai prototype-nya.
Constructor, yaitu fungsi yang namanya biasanya dimulai dengan huruf besar, bisa dipakai dengan operator new buat bikin objek baru. Prototype objek barunya bakal jadi objek yang ditemuin di properti prototype constructor-nya. Kamu bisa memanfaatkan ini dengan naruh properti yang dibagi semua nilai dari tipe tertentu ke prototype mereka. Ada notasi class yang nyediain cara jelas buat mendefinisikan constructor dan prototype-nya.
Kamu bisa mendefinisikan getter dan setter buat diam-diam manggil method setiap kali properti objek diakses. Method static adalah method yang disimpan di constructor class bukannya di prototype-nya.
Operator instanceof bisa, dikasih objek dan constructor, ngasih tahu kamu apakah objek itu instance dari constructor itu.
Satu hal berguna yang bisa dilakuin dengan objek adalah nentuin interface buat mereka dan ngasih tahu semua orang bahwa mereka seharusnya ngomong ke objekmu cuma lewat interface itu. Sisa detail yang nyusun objekmu sekarang dienkapsulasi, tersembunyi di balik interface. Kamu bisa pakai properti privat buat nyembunyiin bagian dari objekmu dari dunia luar.
Lebih dari satu tipe bisa ngimplementasiin interface yang sama. Kode yang ditulis buat pakai sebuah interface otomatis tahu cara kerja dengan berapa pun objek berbeda yang nyediain interface-nya. Ini disebut polymorphism.
Waktu ngimplementasiin beberapa class yang cuma beda di beberapa detail, bisa membantu buat nulis class barunya sebagai subclass dari class yang udah ada, mewarisi sebagian perilakunya.
Poin-poin pentingnya:
- Method itu properti berisi fungsi;
thisnunjuk ke objek tempat method dipanggil. Arrow function gak punyathissendiri (minjem dari luar),functionbiasa punya. - Objek punya prototype: kalau properti gak ketemu di objek, dicari di prototype-nya, terus ke atas sampai
Object.prototype. class= constructor + prototype.newbikin instance baru. Properti#privatcuma bisa diakses di dalam class.- Jangan pakai objek biasa sebagai map (ada
toStringdari prototype), pakaiMapatauObject.create(null). Cek properti milik sendiri pakaiObject.hasOwn. - Polymorphism: kode yang ngarepin interface tertentu bisa kerja dengan objek apa pun yang nyediain interface itu.
get/setbuat properti yang sebenernya method,staticbuat method di constructor.- Symbol itu nama properti unik;
Symbol.iterator+ methodnext()yang ngembaliin{value, done}bikin objek bisa dipakai difor/ofdan.... extends+superbuat inheritance, tapi jangan berlebihan.instanceofbuat ngecek class.
Latihan
Tipe Vector
Tulis class Vec yang mewakili vektor di ruang dua dimensi. Dia ngambil parameter x dan y (angka), yang dia simpan ke properti dengan nama yang sama.
Kasih prototype Vec dua method, plus dan minus, yang ngambil vektor lain sebagai parameter dan ngembaliin vektor baru yang punya jumlah atau selisih dari nilai x dan y kedua vektor (this dan parameternya).
Tambahin properti getter length ke prototype yang ngitung panjang vektornya, yaitu, jarak titik (x, y) dari titik asal (0, 0).
// Kodemu di sini.
console.log(new Vec(1, 2).plus(new Vec(2, 3)));
// → Vec{x: 3, y: 5}
console.log(new Vec(1, 2).minus(new Vec(2, 3)));
// → Vec{x: -1, y: -1}
console.log(new Vec(3, 4).length);
// → 5Petunjuk:
Lihat lagi contoh class Rabbit kalau kamu gak yakin bentuk deklarasi class.
Nambahin properti getter ke constructor bisa dilakuin dengan naruh kata get sebelum nama method. Buat ngitung jarak dari (0, 0) ke (x, y), kamu bisa pakai teorema Pythagoras, yang bilang bahwa kuadrat dari jarak yang kita cari sama dengan kuadrat koordinat x ditambah kuadrat koordinat y. Jadi, √(x^2 + y^2) adalah angka yang kamu mau. Math.sqrt adalah cara ngitung akar kuadrat di JavaScript dan x ** 2 bisa dipakai buat ngkuadratin angka.
Solusi:
class Vec {
constructor(x, y) {
this.x = x;
this.y = y;
}
plus(other) {
return new Vec(this.x + other.x, this.y + other.y);
}
minus(other) {
return new Vec(this.x - other.x, this.y - other.y);
}
get length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
console.log(new Vec(1, 2).plus(new Vec(2, 3)));
// → Vec{x: 3, y: 5}
console.log(new Vec(1, 2).minus(new Vec(2, 3)));
// → Vec{x: -1, y: -1}
console.log(new Vec(3, 4).length);
// → 5Group
Environment JavaScript standar nyediain struktur data lain bernama Set. Kaya instance Map, set megang kumpulan nilai. Beda dengan Map, dia gak mengaitkan nilai lain dengan mereka, dia cuma ngelacak nilai mana yang jadi bagian dari set. Sebuah nilai cuma bisa jadi bagian dari set sekali, nambahinnya lagi gak punya efek apa pun.
Tulis class bernama Group (karena Set udah dipakai). Kaya Set, dia punya method add, delete, dan has. Constructor-nya bikin group kosong, add nambahin nilai ke group (tapi cuma kalau belum jadi anggota), delete ngehapus argumennya dari group (kalau dia anggota), dan has ngembaliin nilai Boolean yang nunjukin apakah argumennya anggota group.
Pakai operator ===, atau yang setara kaya indexOf, buat nentuin apakah dua nilai sama.
Kasih class-nya method static from yang ngambil objek iterable sebagai argumennya dan bikin group yang berisi semua nilai yang dihasilin dari iterasi dia.
class Group {
// Kodemu di sini.
}
let group = Group.from([10, 20]);
console.log(group.has(10));
// → true
console.log(group.has(30));
// → false
group.add(10);
group.delete(10);
console.log(group.has(10));
// → falsePetunjuk:
Cara termudah buat ngelakuin ini adalah nyimpen array anggota group di properti instance. Method includes atau indexOf bisa dipakai buat ngecek apakah nilai tertentu ada di array.
Constructor class-mu bisa nyetel koleksi anggotanya ke array kosong. Waktu add dipanggil, dia harus ngecek apakah nilai yang dikasih ada di array atau nambahinnya kalau gak, mungkin pakai push.
Ngehapus elemen dari array, di delete, kurang lugas, tapi kamu bisa pakai filter buat bikin array baru tanpa nilainya. Jangan lupa nimpa properti yang megang anggotanya dengan versi array yang baru difilter.
Method from bisa pakai loop for/of buat ngambil nilai dari objek iterable dan manggil add buat naruhnya ke group yang baru dibikin.
Solusi:
class Group {
#members = [];
add(value) {
if (!this.has(value)) {
this.#members.push(value);
}
}
delete(value) {
this.#members = this.#members.filter(v => v !== value);
}
has(value) {
return this.#members.includes(value);
}
static from(collection) {
let group = new Group;
for (let value of collection) {
group.add(value);
}
return group;
}
}
let group = Group.from([10, 20]);
console.log(group.has(10));
// → true
console.log(group.has(30));
// → false
group.add(10);
group.delete(10);
console.log(group.has(10));
// → falseGroup yang Iterable
Bikin class Group dari latihan sebelumnya jadi iterable. Lihat bagian tentang interface iterator sebelumnya di bab ini kalau kamu udah gak jelas soal bentuk persis interface-nya.
Kalau kamu pakai array buat mewakili anggota group, jangan cuma ngembaliin iterator yang dibikin dengan manggil method Symbol.iterator di array-nya. Itu bakal jalan, tapi mengalahkan tujuan latihan ini.
Gak apa-apa kalau iterator-mu berperilaku aneh waktu group-nya dimodifikasi selama iterasi.
// Kodemu di sini (dan kode dari latihan sebelumnya)
for (let value of Group.from(["a", "b", "c"])) {
console.log(value);
}
// → a
// → b
// → cPetunjuk:
Mungkin berguna buat mendefinisikan class baru GroupIterator. Instance iterator harus punya properti yang ngelacak posisi saat ini di group. Setiap kali next dipanggil, dia ngecek apakah udah selesai dan, kalau belum, pindah melewati nilai saat ini dan ngembaliinnya.
Class Group sendiri dapet method yang dinamai dengan Symbol.iterator yang, waktu dipanggil, ngembaliin instance baru dari class iterator buat group itu.
Solusi:
class Group {
#members = [];
add(value) {
if (!this.has(value)) {
this.#members.push(value);
}
}
delete(value) {
this.#members = this.#members.filter(v => v !== value);
}
has(value) {
return this.#members.includes(value);
}
static from(collection) {
let group = new Group;
for (let value of collection) {
group.add(value);
}
return group;
}
[Symbol.iterator]() {
return new GroupIterator(this.#members);
}
}
class GroupIterator {
#members;
#position;
constructor(members) {
this.#members = members;
this.#position = 0;
}
next() {
if (this.#position >= this.#members.length) {
return {done: true};
} else {
let result = {value: this.#members[this.#position],
done: false};
this.#position++;
return result;
}
}
}
for (let value of Group.from(["a", "b", "c"])) {
console.log(value);
}
// → a
// → b
// → cMinjem Method (dari edisi ke-3)
Latihan ini ada di edisi ke-3 buku dan masih tersedia di sandbox website, jadi saya sertain juga.
Sebelumnya di bab ini saya nyebutin bahwa hasOwnProperty sebuah objek bisa dipakai sebagai alternatif yang lebih kuat dari operator in kalau kamu mau ngabaikan properti prototype. Tapi gimana kalau map-mu perlu nyertain kata "hasOwnProperty"? Kamu gak bakal bisa manggil method itu lagi karena properti milik objek sendiri nyembunyiin nilai method-nya.
Bisa kamu pikirin cara buat manggil hasOwnProperty di objek yang punya properti sendiri dengan nama itu?
let map = {one: true, two: true, hasOwnProperty: true};
// Perbaiki pemanggilan ini
console.log(map.hasOwnProperty("one"));
// → trueSolusi:
let map = {one: true, two: true, hasOwnProperty: true};
console.log(Object.prototype.hasOwnProperty.call(map, "one"));
// → true