Pewarisan prototipe
Dengan pengecualian null
dan undefined
, setiap jenis data primitif memiliki
prototipe, wrapper objek yang sesuai yang menyediakan metode untuk menangani
nilai. Saat pencarian metode atau properti dipanggil pada primitif,
JavaScript akan menggabungkan primitif di balik layar dan memanggil metode atau
melakukan pencarian properti pada objek wrapper.
Misalnya, literal string tidak memiliki metode sendiri, tetapi Anda dapat memanggil
metode .toUpperCase()
di dalamnya berkat wrapper objek
String
yang sesuai:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
Hal ini disebut pewarisan prototipe—mewarisi properti dan metode dari konstruktor nilai yang sesuai.
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
Anda dapat membuat primitif menggunakan konstruktor ini, bukan hanya menentukannya
berdasarkan nilainya. Misalnya, penggunaan konstruktor String
akan membuat
objek string, bukan literal string: objek yang tidak hanya berisi nilai
string, namun semua properti dan metode yang diwarisi dari konstruktor.
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
Untuk sebagian besar, objek yang dihasilkan berperilaku sebagai nilai yang kami gunakan untuk
menentukannya. Misalnya, meskipun menentukan nilai angka menggunakan
konstruktor new Number
menghasilkan objek yang berisi semua metode dan
properti prototipe Number
, Anda dapat menggunakan operator matematika pada
objek tersebut seperti yang Anda lakukan pada literal angka:
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
Anda akan sangat jarang menggunakan konstruktor ini, karena pewarisan prototipe bawaan JavaScript berarti bahwa konstruktor tersebut tidak memberikan manfaat praktis. Membuat primitif menggunakan konstruktor juga dapat menyebabkan hasil yang tidak diharapkan, karena hasilnya adalah objek, bukan literal sederhana:
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
Hal ini dapat mempersulit penggunaan operator perbandingan yang ketat:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
Penyisipan titik koma otomatis (ASI)
Saat mengurai skrip, penafsir JavaScript dapat menggunakan fitur yang disebut penyisipan titik koma otomatis (ASI) untuk mencoba memperbaiki instance titik koma yang dihilangkan. Jika parser JavaScript menemukan token yang tidak diizinkan, parser akan mencoba menambahkan titik koma sebelum token tersebut untuk memperbaiki potensi error sintaksis, selama satu atau beberapa kondisi berikut terpenuhi:
- Token tersebut dipisahkan dari token sebelumnya dengan baris baru.
- Token itu adalah
}
. - Token sebelumnya adalah
)
, dan titik koma yang disisipkan akan menjadi titik koma akhir dari pernyataando
...while
.
Untuk mengetahui informasi selengkapnya, baca aturan ASI.
Misalnya, menghilangkan titik koma setelah pernyataan berikut tidak akan menyebabkan error sintaksis karena ASI:
const myVariable = 2
myVariable + 3
> 5
Namun, ASI tidak dapat memperhitungkan beberapa laporan di baris yang sama. Jika Anda menulis lebih dari satu pernyataan pada baris yang sama, pastikan untuk memisahkannya dengan titik koma:
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
ASI adalah upaya memperbaiki error, bukan semacam fleksibilitas sintaksis yang dibangun ke dalam JavaScript. Pastikan untuk menggunakan titik koma jika sesuai sehingga Anda tidak mengandalkannya untuk menghasilkan kode yang benar.
Strict Mode
Standar yang mengatur cara penulisan JavaScript telah berkembang jauh melampaui apa pun yang dipertimbangkan selama desain awal bahasa ini. Setiap perubahan baru pada perilaku JavaScript yang diharapkan harus menghindari terjadinya error di situs lama.
ES5 mengatasi beberapa masalah yang sudah lama ada pada semantik JavaScript tanpa mengganggu implementasi yang ada dengan memperkenalkan "mode ketat", yakni cara untuk menggunakan sekumpulan aturan bahasa yang lebih ketat untuk seluruh skrip atau fungsi individual. Untuk mengaktifkan mode ketat, gunakan literal string
"use strict"
, diikuti dengan titik koma, di baris pertama skrip atau
fungsi:
"use strict";
function myFunction() {
"use strict";
}
Mode ketat mencegah tindakan "tidak aman" tertentu atau fitur yang tidak digunakan lagi, menampilkan error eksplisit sebagai pengganti tindakan "silent" yang umum, dan melarang penggunaan sintaksis yang mungkin bertabrakan dengan fitur bahasa di masa mendatang. Misalnya, keputusan desain awal terkait cakupan variabel membuat developer lebih mungkin untuk salah "memengaruhi" cakupan global saat mendeklarasikan variabel, terlepas dari konteksnya, dengan menghilangkan kata kunci var
:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
Runtime JavaScript modern tidak dapat memperbaiki perilaku ini tanpa risiko merusak situs apa pun yang mengandalkannya, baik secara tidak sengaja, maupun secara sengaja. Sebaliknya, JavaScript modern mencegahnya dengan mengizinkan developer menggunakan mode ketat untuk pekerjaan baru, dan mengaktifkan mode ketat secara default hanya dalam konteks fitur bahasa baru yang tidak akan merusak implementasi lama:
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
Anda harus menulis "use strict"
sebagai
literal string.
Literal template
(use strict
) tidak akan berfungsi. Anda juga harus menyertakan "use strict"
sebelum
kode yang dapat dieksekusi dalam konteks yang diinginkan. Jika tidak, penafsir akan mengabaikannya.
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
Berdasarkan referensi, berdasarkan nilai
Setiap variabel, termasuk properti objek, parameter fungsi, dan elemen dalam array, set, atau peta, dapat berisi nilai primitif atau nilai referensi.
Saat nilai primitif ditetapkan dari satu variabel ke variabel lainnya, mesin JavaScript akan membuat salinan nilai tersebut dan menetapkannya ke variabel.
Saat Anda menetapkan sebuah objek (instance class, array, dan fungsi) ke suatu variabel, bukannya membuat salinan baru dari objek tersebut, variabel tersebut akan memuat referensi ke posisi objek yang disimpan di memori. Oleh karena itu, mengubah objek yang direferensikan oleh variabel akan mengubah objek yang direferensikan, bukan hanya nilai yang terdapat dalam variabel tersebut. Misalnya, jika Anda menginisialisasi variabel baru dengan variabel yang berisi referensi objek, kemudian menggunakan variabel baru tersebut untuk menambahkan properti ke objek tersebut, properti dan nilainya akan ditambahkan ke objek asli:
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
Hal ini tidak hanya penting untuk mengubah objek, tetapi juga melakukan perbandingan
yang ketat, karena kesetaraan yang ketat antar-objek memerlukan kedua variabel untuk
mereferensikan objek yang sama agar dievaluasi ke true
. Objek tersebut tidak dapat mereferensikan
objek yang berbeda, meskipun objek tersebut identik secara struktural:
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
Alokasi memori
JavaScript menggunakan pengelolaan memori otomatis, yang berarti bahwa memori tidak perlu dialokasikan atau dibatalkan alokasinya secara eksplisit selama pengembangan. Meskipun detail pendekatan mesin JavaScript terhadap pengelolaan memori berada di luar cakupan modul ini, memahami cara alokasi memori akan memberikan konteks yang berguna untuk menangani nilai referensi.
Ada dua "area" dalam memori: "stack" dan "heap". Tumpukan menyimpan data statis, yaitu nilai dasar dan referensi ke objek, karena jumlah ruang tetap yang diperlukan untuk menyimpan data ini dapat dialokasikan sebelum skrip dieksekusi. Heap menyimpan objek, yang memerlukan ruang yang dialokasikan secara dinamis karena ukurannya dapat berubah selama eksekusi. Memori dibebaskan oleh proses yang disebut "pembersihan sampah memori", yang menghapus objek tanpa referensi dari memori.
Thread utama
JavaScript adalah bahasa thread tunggal pada dasarnya dengan model eksekusi "sinkron", yang berarti JavaScript hanya dapat mengeksekusi satu tugas dalam satu waktu. Konteks eksekusi berurutan ini disebut thread utama.
Thread utama digunakan bersama oleh tugas browser lainnya, seperti mengurai HTML, merender, dan merender ulang bagian halaman, menjalankan animasi CSS, serta menangani interaksi pengguna mulai dari yang sederhana (seperti menandai teks) hingga yang kompleks (seperti berinteraksi dengan elemen formulir). Vendor browser telah menemukan cara untuk mengoptimalkan tugas yang dilakukan oleh thread utama, tetapi skrip yang lebih kompleks masih dapat menggunakan terlalu banyak resource thread utama dan memengaruhi performa halaman secara keseluruhan.
Beberapa tugas dapat dijalankan di thread latar belakang yang disebut Web Worker, dengan beberapa batasan:
- Thread pekerja hanya dapat bertindak pada file JavaScript mandiri.
- Akses ke jendela browser dan UI telah berkurang secara signifikan atau tidak ada sama sekali.
- Kemampuan komunikasi dengan thread utama terbatas.
Batasan ini membuatnya ideal untuk tugas yang terfokus dan membutuhkan banyak resource yang mungkin menempati thread utama.
Stack panggilan
Struktur data yang digunakan untuk mengelola "konteks eksekusi"—kode yang dieksekusi secara aktif—adalah daftar yang disebut stack panggilan (sering kali hanya "tumpukan"). Saat skrip pertama kali dijalankan, penafsir JavaScript akan membuat "konteks eksekusi global" dan mengirimnya ke stack panggilan, dengan pernyataan dalam konteks global tersebut dijalankan satu per satu, dari atas ke bawah. Saat penafsir menemukan panggilan fungsi saat mengeksekusi konteks global, penafsir akan mengirim "konteks eksekusi fungsi" untuk panggilan tersebut ke bagian atas stack, menjeda konteks eksekusi global, dan mengeksekusi konteks eksekusi fungsi.
Setiap kali fungsi dipanggil, konteks eksekusi fungsi untuk panggilan tersebut didorong ke bagian atas stack, tepat di atas konteks eksekusi saat ini. Stack panggilan beroperasi dengan basis "pertama masuk, pertama keluar", yang berarti bahwa panggilan fungsi terbaru, yang tertinggi dalam stack, dijalankan dan berlanjut hingga selesai. Setelah fungsi tersebut selesai, penafsir akan menghapusnya dari stack panggilan, dan konteks eksekusi yang berisi panggilan fungsi tersebut akan menjadi item tertinggi dalam stack lagi dan melanjutkan eksekusi.
Konteks eksekusi ini menangkap nilai apa pun yang diperlukan untuk eksekusinya. Aturan ini juga menetapkan variabel dan fungsi yang tersedia dalam cakupan fungsi berdasarkan konteks induknya, serta menentukan dan menetapkan nilai kata kunci this
dalam konteks fungsi.
Loop peristiwa dan antrean callback
Eksekusi berurutan ini berarti bahwa tugas asinkron yang menyertakan fungsi
callback, seperti mengambil data dari server, merespons interaksi pengguna,
atau menunggu timer yang disetel dengan setTimeout
atau setInterval
, akan memblokir
thread utama hingga tugas tersebut selesai, atau secara tidak terduga mengganggu
konteks eksekusi saat ini saat konteks eksekusi fungsi callback
ditambahkan ke stack. Untuk mengatasinya, JavaScript mengelola tugas asinkron menggunakan "model konkurensi" berbasis peristiwa yang terdiri dari "loop peristiwa" dan "antrean callback" (terkadang disebut sebagai "antrean pesan").
Saat tugas asinkron dieksekusi di thread utama, konteks eksekusi fungsi callback ditempatkan dalam antrean callback, bukan di atas stack panggilan. Loop peristiwa adalah pola yang terkadang disebut reaktor, yang terus-menerus memeriksa status stack panggilan dan antrean callback. Jika ada tugas dalam antrean callback dan loop peristiwa menetapkan bahwa stack panggilan kosong, tugas dari antrean callback akan didorong ke stack satu per satu untuk dijalankan.