Pernah melihat teks Café padahal harusnya Café? Atau emoji yang jadi ??? Atau "😀".length mengembalikan 2 di JavaScript? Semua itu gara-gara character encoding — cara komputer mengubah huruf menjadi byte (dan sebaliknya).
Sejarah singkat encoding:
| Era | Encoding | Karakter didukung |
|---|---|---|
| 1960an | ASCII | 128 karakter (A-Z, a-z, 0-9, simbol dasar) — 7 bit |
| 1980an | Latin-1 (ISO-8859-1) | 256 karakter (ASCII + huruf Eropa Barat: é, ñ, ü) — 8 bit |
| 1990an | Kode halaman nasional | Windows-1252, Shift-JIS, GB2312 (Cina) — kacau untuk web internasional |
| 1991+ | Unicode | Lebih dari 149.000 karakter semua bahasa + emoji |
Unicode = standar universal:
Unicode memberi setiap karakter sebuah code point — angka unik yang ditulis U+XXXX:
U+0041→AU+00E9→éU+1F600→😀U+4E2D→中
Unicode hanya mendefinisikan peta angka ke karakter. Bagaimana angka itu disimpan sebagai byte adalah urusan encoding: UTF-8, UTF-16, atau UTF-32.
UTF-8 — juara web:
UTF-8 adalah encoding variable-length: 1 sampai 4 byte per karakter.
| Range code point | Byte |
|---|---|
| U+0000 – U+007F (ASCII) | 1 byte |
| U+0080 – U+07FF | 2 byte |
| U+0800 – U+FFFF | 3 byte |
| U+10000 – U+10FFFF | 4 byte |
"A" (U+0041) → 0x41 (1 byte)
"é" (U+00E9) → 0xC3 0xA9 (2 byte)
"中" (U+4E2D) → 0xE4 0xB8 0xAD (3 byte)
"😀" (U+1F600) → 0xF0 0x9F 0x98 0x80 (4 byte)
Keunggulan UTF-8:
- Backward compatible dengan ASCII — teks Inggris murni tetap 1 byte/karakter
- Self-synchronizing — kalau byte rusak, parser bisa re-sync tanpa corrupt seluruh file
- Tidak ada BOM wajib — lebih sederhana
- Dominan di web — 98% website pakai UTF-8
UTF-16 — dipakai internal JavaScript, Java, Windows:
UTF-16 pakai unit 2 byte (16 bit). Karakter yang masuk 16 bit (BMP) = 1 unit. Yang di luar BMP (emoji!) butuh surrogate pair = 2 unit.
"A" → 1 unit (2 byte total)
"中" → 1 unit (2 byte total)
"😀" → 2 unit (4 byte total, surrogate pair)
Mengapa "😀".length === 2 di JavaScript?
JavaScript string adalah urutan UTF-16 code unit, bukan code point. length mengembalikan jumlah unit.
"A".length // 1
"中".length // 1
"😀".length // 2 ← surrogate pair
"👨👩👧".length // 8 ← emoji keluarga = banyak surrogate + zero-width joiner
Cara menghitung karakter sebenarnya:
[..."😀"].length // 1 (array iterator = code point)
[..."👨👩👧"].length // 5
Array.from("😀").length // 1
new Intl.Segmenter().segment("👨👩👧") // handles grapheme clusters
Code point vs code unit vs byte — jangan bingung:
- Code point: nilai angka Unicode untuk satu karakter (
U+1F600) - Code unit: unit terkecil dari encoding (UTF-16 = 16 bit, UTF-8 = 8 bit)
- Byte: 8 bit, hasil akhir di memori / disk
Untuk 😀 (U+1F600):
- 1 code point
- 2 code unit di UTF-16, atau 4 code unit di UTF-8
- 4 byte di UTF-16, atau 4 byte di UTF-8
Percent encoding (URL encoding):
URL hanya boleh mengandung ASCII "aman". Karakter lain harus di-encode:
Spasi → %20
é → %C3%A9 (byte UTF-8 di-encode)
中 → %E4%B8%AD
😀 → %F0%9F%98%80
?, &, = → %3F, %26, %3D (punya makna khusus di URL)
encodeURIComponent("hello world"); // "hello%20world"
encodeURIComponent("café"); // "caf%C3%A9"
encodeURIComponent("😀"); // "%F0%9F%98%80"
decodeURIComponent("%F0%9F%98%80"); // "😀"
encodeURI vs encodeURIComponent:
encodeURI— untuk URL lengkap, tidak encode: / ? & = #encodeURIComponent— untuk query value, encode semua karakter khusus
Gunakan encodeURIComponent untuk nilai parameter:
const query = "cari&tampilkan=true";
const url = `/search?q=${encodeURIComponent(query)}`;
// "/search?q=cari%26tampilkan%3Dtrue" ← aman
Deklarasi encoding di HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"> <!-- WAJIB, harus dalam 1024 byte pertama -->
<title>...</title>
</head>
Tanpa deklarasi, browser menebak — dan sering salah.
Deklarasi encoding di HTTP:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
HTTP header menang atas meta tag. Pastikan server kamu selalu kirim charset=utf-8.
BOM (Byte Order Mark):
BOM adalah 3 byte 0xEF 0xBB 0xBF di awal file UTF-8 — penanda "ini UTF-8."
Masalahnya: BOM bisa merusak banyak hal (PHP output error, CSV parser bingung, JSON gagal parse). Untuk UTF-8, jangan pakai BOM.
<?php
// File dengan BOM:
↑ 3 byte tersembunyi di sini bisa bikin "headers already sent" error
Mojibake — bug encoding klasik:
Mojibake (文字化け, "huruf rusak" dalam Jepang) terjadi saat byte di-decode dengan encoding salah:
Teks asli UTF-8: "Café" (byte: 43 61 66 C3 A9)
Di-decode sebagai Latin-1: "Café" ← bug!
Cara menghindari:
- Gunakan UTF-8 di semua lapisan: database, HTTP header, meta tag, source file
- MySQL: pakai
utf8mb4(UTF-8 penuh, termasuk emoji) — bukanutf8(yang cuma 3 byte) - PHP:
mb_*functions,header("Content-Type: text/html; charset=utf-8") - Node.js: default UTF-8, tapi hati-hati dengan
Buffer.toString()— selalu specify encoding - File source code: simpan sebagai UTF-8 tanpa BOM
Contoh bug emoji length:
const name = "😀Budi";
if (name.length <= 10) { // BUG: emoji dihitung 2 unit
// "😀Budi" punya length 6, tapi visually 5 karakter
}
// Solusi:
const charCount = [...name].length; // 5 — benar!
Contoh konversi byte ke teks:
const encoder = new TextEncoder(); // default UTF-8
const bytes = encoder.encode("Halo 😀");
console.log(bytes); // Uint8Array [72, 97, 108, 111, 32, 240, 159, 152, 128]
const decoder = new TextDecoder("utf-8");
const text = decoder.decode(bytes);
console.log(text); // "Halo 😀"
Tips praktis:
- Default ke UTF-8 di mana-mana — browser, server, DB, file
- Pakai
utf8mb4di MySQL untuk emoji dan karakter BMP+ - Untuk validasi length, gunakan grapheme counter bukan
.length - Untuk URL, selalu
encodeURIComponentuntuk nilai dinamis - Kalau file terlihat rusak, buka dengan hex editor — 3 byte pertama
EF BB BF= ada BOM
Encoding adalah hal yang mudah dilupakan sampai terjadi bug — lalu sulit di-debug. Investasi waktu sekarang untuk memahaminya.