25 March 2024

Memahami javascript

Image by Edwin Andrade

Sedikit sejarah

Javascript dibuat oleh Brendan Eich pada 1995 yang digunakan untuk browser milik Netscape. Pada masa itu, belum ada scripting language yang digunakan untuk browser dan semua interaksi web termasuk validasi input field harus dilakukan di server karena web hanya ada HTML. Karena internet sangat lambat, interaksi web jadi makin lambat kalo validasi input aja harus di server. Maka diciptakanlah javascript supaya biar bisa validasi input di browser yang jadinya bisa scripting language untuk browser. Jadi javascript itu di inisiasi karena biar bisa validasi input di browser wkwkw.

ECMA

Saat javascript mulai populer digunakan di browser, dibutuhkan standart agar javascript dapat diimplementasi secara konsisten di berbagai browser. Akhirnya standart itu dibuat oleh European Computer Manufacture Association (ECMA) pada 1997. Saat ini, ECMAScript adalah sebuah standart yang dibuat untuk sebuah bahasa scripting seperti javascript atau actionScript Jadi ECMA bukan cuma berlaku di javascript (walaupun saat ini tidak banyak bahasa yang berbasi ECMAScript). Jadi yang sering kita dengar mengenai ES6 atau ES2015 adalah sebuah standart yang dikeluarkan oleh ECMA pada tahun tersebut dan akan diimpleemntasikan pada browser yang menggunakan scrpting language seperti javascript. Jadi kalo ada browser yang tidak kompatibel dengan kode javascript yang terbaru ya karena mereka punya tool atau engine tersendiri yang mereka gunakan untuk mengeksekusi javascript dan tools mereka belum mengaplikasikan standart yang baru.

Paradigma

Javascript adalah prototype based language. Prototype sendiri sebenarnya adalah sebuah paradigma pemrograman yang mirip seperti OOP, bedanya di paradigma prototype metode inheritance dapat lebih dinamis. Tidak seperti classical OOP dimana inheritance dilakukan secara hirarkikal (parent-child), pada paradigma protoype inheritance dapat dilakukan horizontal (antar object yang tidak memiliki hubungan hirarki).

Engine dan Runtime

Untuk memahami bagaimana javascript bekerja, kita harus memahami javascript engine dan javascript runtime. Dua hal tersebut sangat erat kaitaanya terhadap bagaimana javascript bekerja. Javascript engine dapat bekerja tanpa runtime, akan tetapi runtime tidak dapat bekerja tanpa engine.

Sederhananya, engine itu kayak mesin pada mobil sedangkan runtime adalah hal-hal tambahan yang membuat mobil adalah mobil seperti 4 roda, rem, kursi, dsb. Kita bisa aja mengambil mesin pada mobil dan digunakan pada kendaraan lain seperti motor kayak motor-motor ini.

Intinya, engine berfungsi sebagai proses compilation kode javascript sedangkan runtime adalah environtment yang memberikan fungsionalitas tambahan agar javascript dapat digunakan untuk membantu membangun aplikasi.

Javascript engine

Javascript engine merupakan inti pada javascript, alat inilah yang sebenarnya digunakan untuk mengeksekusi code javascript kita sehingga bisa dijalankan oleh hardware. Ada cukup banyak js engine yang tersedia saat ini, seperti v8 yang digunakan di google chrome, spidermonkey yang digunakan untuk firefox, maupun untuk keperluan IoT seperti jerryscript.

Walaupun memiliki engine yang beragam, tapi semua js engine memiliki syntax yang sama karena mengikuti aturan ECMAScript. Aturan ECMAScript di implementasikan di js engine, sedangkan semua yang tidak ada di aturan yang dibuat ECMA biasanya diimplementasikan di level runtime. Salah satu contohnya adalah console.log, object console tidak ada di aturan ECMA tapi itu sering kita gunakan. console.log tidak ada di engine seperti v8, akan tetapi itu diimplementasi oleh runtime seperti browser atau node.

JS Engine internal

JS engine flow

Apa yang terjadi saat kode javascript kita dieksekusi?

Parsing

Hal pertama yang dilakukan adalah menentukan setiap token yang ada didalam kode kita. Apa itu token? token adalah basic unit atau element terkecil yang sudah didefiniskan untuk sebuah bahasa. Setiap karakter bahkan kata yang kita tulis dalam javascript dapat membentuk token. Berikut contoh token yang didefiniskan dalam javascript

  • Token keyword

    • let
    • var
    • function
    • new
    • const
  • Token operator

    • +
    • -
    • *
    • /

Masih banyak token-token yang ada dalam js, nama variable itu termasuk token yang dinaman IDENTIFIER, bahkan value untuk variable juga token seperti string, number, atau boolean.

Didalam proses parsing ini juga ditentukan apakah source kita valid atau sudah sesuai grammar atau tidak. Apa itu grammar? grammar itu seperti pada layaknya bahasa manusia, yaitu merupakan tata bahasa.

// Tidak valid karena tidak sesuai dengan grammar
let = a "halo";

// Valid
let a = "halo";

Hasil dari proses parser adalah AST atau abstract syntax tree. Hasil AST ini akan digunakan di proses compilation.

// Contoh AST dari kode
let a = "halo";

// source: esprima.org

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": "halo",
            "raw": ""halo""
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "script"
}

Hasil AST ini akan diproses lagi untuk membentuk bytecode.

Compilation

Proses compilation javascript dapat berbeda-beda tergantung bagaimana engine dibuat. Pada saat pertama kali dibuat, js adalah interpreted language, namun pada saat ini javascript dibuat dengan metode JIT.

Sebelum memahami apa itu JIT, kita perlu paham apa itu interpreted language dan compiled language. Compiled language adalah bahasa yang di compiled oleh compiler sedangkan interpreted language adalah bahasa yang di interpretasikan oleh interpreter. Hmmm, cukup membingungkan yaa, compiled itu sendiri berarti setiap kode yang ditulis akan dibuah langsung kedalam bahasa mesin sedangkan interpreted artinya setiap line yang ditulis dalam kode kita akan diterjemahkan oleh interpreter dan akan dieksekusi oleh si interpreter.

Contoh compiled language adalah c atau c++, ketika kita membuat kode c maka kita harus mengcompiled kode tersebut kedalam mechine code lalu baru kita dapat mengsekusi dari binary yang sudah di compiled. Tugas si compiler dalam compiled language adalah menerjemahkan kode c kedalam bahasa mesin.

Contoh interpreted language adalah python atau versi awal javascript. Ketika kita menulis kode python dan akan mengeksekusi kode tersebut, interpreted yang ditulis dalam bahasa C akan mengiinterpretasi kode python kita kedalam bahaca C lalu akan dieksekusi baris perbaris. Dengan model seperti ini, salah satu alasan mengapa banyak bahasa yang dynamically typed itu dibuat secara interpreted. Karena dalam dynamic type language, akan lebih mudah untuk mengalokasikan memori jika kode kita dieksekusi baris perbaris.

var a = 10;
a = "Halo";

Untuk membuat sebuah variable kita harus menentukan berapa banyak memory yang akan digunakan untuk menyimpan variable tersebut. Dalam bahasa seperti C, hal ini lebih jelas karena dalam mendefiniskan variable kita juga mendefinisikan tipe datanya yang juga berpengaruh seberapa besar memory yang dibutuhkan untuk menyimpan variable dengan tipe data tersebut. Tapi kalo interpreted ketika baris pertama dieksekusi, interpreter akan mengecek tipe data yang dimasukan kedalam variable a dan akan mengalokasikan memory untuk tipe integer, lalu saat mengeksekusi varis kedua interpreter dapat membuat alokasi baru untuk tipe data string dengan panjang 4 karakter. Dikarenakan kemudahan dalam melakukan alokasi memory secara dinamis, itulah kenapa bahasa dengan tipe dinamis akan dieksekusi secara interpreted.

Lalu apa itu JIT? JIT atau Just-in-Time compilation adalah gabungan dari keduanya. Kode kita akan dieksekusi secara interpreted akan tetapi ada compiler yang dapat mengoptimisasi proses-proses yang sekiranya lambat.

V8 engine

Proses diatas adalah structure milik javascript engine milik google yaitu V8 yang digunakan di google chrome maupun nodejs. V8 memiliki interpreted yang bernama ignition, ignition akan menginterpretasi AST ke bytecode dan ketika proses eksekusi menemukan fungsi yang sangat intense atau dijalankan cukup sering maka TurboFan atau compiler milik V8 akan mengubah bytecode dari fungsi yang sering dijalankan itu kedalam bahasa mesin sehingga ketika fungsi itu dijalankan lagi V8 tidak tinggal memanggil machine code yang sudah dibuat turbonfan. Dengan cara itu, javascript dapat berjalan dengan lebih cepat daripada menggunakan interpreter saja.

Excecution

Stack

Ada dua komponen penting dalam js engine yaitu call stack dan heap. Call stack berfungsi untuk melacak setiap baris kode yang dieksekusi. Sedangkan heap berfungsi sebagai penyimpanan data dynamic yang kita buat dalam kode kita seperti value object atau function. Tidak semua variable yang didefiniskan akan disimpan di heap, variable yang bersifat static seperti string atau boolean akan disimpian di call stack didalam excecution context yang dimana variable tersebut dibuat.

Setiap function yang dijalankan pada javascript akan membuat sebuah function excecution context atau FEC, dimana FEC ini akan menyimpan data mengenai function yang dieksekusi seperti reference maupun setiap variabel yang didefinisikan. Selain itu, excecution context juga dijalankan ketika pertama kali kode javascript di eksekusi yang dinamakan Global Excecution Context (GEC). Jika kita mendifinisikan variable secara global, maka variable tersebut akan ada dalam GEC. Dalam browser, GEC biasanya direpresentasikan dengan object bernama window, sedangkan pada nodejs direpresentasikan dengan object bernama global.

let name = "world";

function helo(name) {
  let obj = {
    str: "world";
  }
  console.log(name + obj.str)
}

hello()

Dalam kode diatas, value dari call stack dan heap akan terlihat seperti ini

Stack result

Runtime

Runtime ini cuma layer tambahan dari js engine sendiri supaya kode javascript bisa digunakan dengan lebih mudah. JS hanya yang ada pada engine hanyalah sekumpulan tata bahasa yang digunakan untuk eksekusi logika sedangkan tools yang kita butuhkan untuk berinteraksi dengan environment seperti bagaimana melakukan network request, melakukan operasi DOM, membaca file. Hal-hal tadi tidak dapat dilakukan secara langsung jika kita hanya mengandalkan js engine. Oleh karena itu kita membutuhkan sebuah runtime yang dapat melakukan hal-hal tersebut. Pada browser kita memiliki fungsi seperti fetch, console.log, setTimeout yang merupakan fungsi-fungsi yang sebenanrya tidak ada di javascript.

Apa yang dilakukan sebuah runtime adalah menambahkan fungsi baru terhadap js engine dengan cara menambahkannya pada global object. Misal jika kita harus menambahkan fungsi Print pada javascript, maka kita harus membuat fungsi tersebut, lalu bind fungsi Print ke global object dan melakukan callback terhadap fungsi yang sudah dibuat jika engine mengeksekusi fungsi Print.