Đếm ký tự JS sai vì surrogate pair | .length vs emoji vs UTF-8【2026】

(Cập nhật: 19 tháng 7, 2026 ) đếm ký tự surrogate pair JavaScript UTF-16 emoji UTF-8 string length
Kết luận

.length trong JavaScript không phải “số ký tự mắt nhìn thấy”. Nó đếm UTF-16 code unit. Emoji như 🍎surrogate pair.length === 2. Cần code point: [...str].length hoặc Array.from(str). Cần “ô chữ” người dùng: Intl.Segmenter. Giới hạn DB/API còn có thể là byte UTF-8. Dán thử trên Đếm ký tự Kawa — ô ký tự ≈ .length, ô byte ≈ TextEncoder — rồi mới tin vào validation.

Fresher Việt paste "Xin chào 🍎" vào form, assert value.length <= 20, QA bảo “chỉ 10 chữ mà vẫn fail”. Console log ra 12 hoặc hơn vì emoji chiếm hai unit. Bug này tái hiện liên tục ở team outsourcing khi client Nhật/Hàn dùng emoji trong title, caption Zalo, hoặc meta description.

Bài này tập trung một góc: hiểu surrogate pair và chọn đúng đơn vị đếm — không phải hướng dẫn chung “cách dùng counter”.

Bài viết này giúp bạn

  • Phân biệt code unit / code point / grapheme / UTF-8 byte
  • Biết vì sao .length “phình” với emoji và một số ký tự
  • Có snippet JS copy được và bảng quyết định theo use case
  • Biết tool Kawa đang đo gì (tránh ảo tưởng “đếm grapheme”)
  • Checklist trước khi merge validation độ dài

Bốn đơn vị đếm — đừng trộn

Đơn vịVí dụ API"🍎".?Khi nào dùng
UTF-16 code unitstr.length2Repro bug JS; một số API cũ đếm UTF-16
Code point[...str].length1Hầu hết “ký tự Unicode” đơn
GraphemeIntl.Segmenter1 (kể cả ZWJ phức tạp hơn)UI, đếm “ô” người dùng nhìn
UTF-8 byteTextEncoder4Payload JSON, cột BYTE, giới hạn HTTP

Tiếng Việt "ế"một code point (hoặc đôi khi tổ hợp base+dấu tùy nguồn nhập). Emoji phần lớn nằm ngoài BMP → hai code unit UTF-16. Đó là surrogate pair.

Surrogate pair là gì (ngắn, đủ dùng)

UTF-16 dùng 16 bit mỗi unit. Ký tự ngoài Basic Multilingual Plane được mã hóa thành cặp:

  • High surrogate: 0xD8000xDBFF
  • Low surrogate: 0xDC000xDFFF

Một emoji = hai phần tử trong representation nội bộ của String. .length đếm từng phần → 2. Spread [...str] iterate theo code point (khi engine xử lý đúng) → 1.

const s = "🍎abc";
console.log(s.length);           // 5  (🍎 = 2 unit)
console.log([...s].length);      // 4  (4 code point)
console.log([...s]);             // ["🍎", "a", "b", "c"]

// ZWJ family — phức tạp hơn
const family = "👨‍👩‍👧";
console.log(family.length);      // thường > 1 (nhiều unit)
console.log([...family].length); // nhiều code point, không phải 1 “ô”

Muốn đếm grapheme (ô nhìn thấy):

function graphemeLength(str) {
  const seg = new Intl.Segmenter("vi", { granularity: "grapheme" });
  return [...seg.segment(str)].length;
}

Intl.Segmenter có trên trình duyệt hiện đại; Node cũ có thể cần polyfill — kiểm tra target runtime trước khi dùng production.


Case study: validation form “tối đa 50 ký tự”

Tình huống: Freelancer nhận brief: bio user tối đa 50 ký tự. Implement if (bio.length > 50) reject.

Fixture thật:

Chuỗi.length[...].lengthGhi chú
Hello55ASCII ổn
Xin chào88khoảng trắng tính
Táo 🍎65emoji +2 unit
🎉🎉🎉63ba emoji = sáu unit

User Việt viết caption ngắn có 3 emoji → frontend báo “quá dài” dù mắt thấy ngắn. Client phàn nàn “app Nhật không bị”. Nguyên nhân: họ đếm grapheme hoặc code point phía server khác frontend.

Cách xử lý:

  1. Thống nhất với PM/BE: đơn vị là gì?
  2. Mirror cùng hàm đếm ở client và server (hoặc chỉ validate server).
  3. Test fixture có emoji + tiếng Việt có dấu trong unit test.

CRLF và khoảng trắng — trap thứ hai

Copy từ Excel/Windows Notepad:

dòng1\r\n
dòng2\r\n

vs paste đã chuẩn hóa:

dòng1\n
dòng2\n

Mỗi dòng Windows thêm một code unit \r. Hai dòng = +2 vào .length. Khi so “số ký tự DB” vs “số trên textarea”, lệch vài đơn vị thường đến từ đây — không phải surrogate.

Quy tắc: trước khi so sánh độ dài với API, text.replace(/\r\n/g, "\n") (hoặc ngược lại nếu server bắt CRLF).


Công cụ đếm ký tự Kawa đo gì?

Đếm ký tự trên trình duyệt chạy local — không upload. Theo implementation hiện tại:

  • Số ký tựString.prototype.length (UTF-16), có tùy chọn bỏ khoảng trắng/xuống dòng trước khi đếm
  • Số dòngsplit(/\r?\n/)
  • Số từ ≈ tách theo khoảng trắng (hữu ích tiếng Anh hơn tiếng Việt không dấu cách từ)
  • Bytesnew TextEncoder().encode(text).length (UTF-8)

Nghĩa là: dán 🍎 → ký tự 2, bytes 4. Đó là feature để debug, không phải bug của tool. Nếu bạn cần grapheme count cho UI, hãy dùng Intl.Segmenter trong code — đừng giả định mọi counter online đều đếm “ô mắt thấy”.

🔢 Đếm ký tự ngay tại đây

Số ký tự

0

Số dòng

0

Số từ

0

Dung lượng (Bytes UTF-8)

0

Gợi ý dùng nhanh: dán cùng một caption Zalo/Facebook, ghi lại cặp (ký tự, byte), rồi đối chiếu preview thật của nền tảng. Nhiều mạng xã hội đếm theo quy tắc riêng với emoji — số trên tool chỉ là baseline JS/UTF-8.


Bảng quyết định: dùng gì khi nào

Chọn đơn vị đếm theo ngữ cảnh
Ngữ cảnh Nên dùng Tránh
Debug vì sao JS length lệch str.length + giải thích surrogate Đổi sang grapheme rồi quên document
Giới hạn 'số ký tự Unicode' API [...str].length / Array.from Tin mù str.length
Đếm ô trên UI (emoji ghép) Intl.Segmenter grapheme Chỉ spread
Cột/payload theo byte TextEncoder UTF-8 Nhầm byte với .length
Meta description SEO sơ bộ Đo + preview Search Console Hard-code 160 bằng .length có emoji

Checklist trước khi merge

  • Comment hoặc schema ghi rõ đơn vị: code unit / code point / grapheme / byte
  • Fixture: ASCII + tiếng Việt có dấu + ít nhất một emoji BMP-ngoài
  • Nếu UI hiển thị “còn X ký tự”, hàm đếm trùng với backend
  • Chuẩn hóa \r\n khi so với file Windows
  • Đã thử trên char-count để thấy .length vs UTF-8 byte cạnh nhau
  • Không copy bio.length > N từ Stack Overflow mà không đọc N nghĩa là gì

Lỗi thường gặp

1. “Emoji làm vỡ substring / slice”
str.slice(0, 1) trên chuỗi bắt đầu bằng emoji có thể cắt giữa surrogate → ký tự lỗi (). Dùng spread hoặc thư viện truncate theo code point/grapheme.

2. “JSON.stringify rồi đếm length”
Dấu ngoặc kép và escape làm lệch so với độ dài hiển thị user.

3. “MySQL utf8 vs utf8mb4”
Charset cũ không lưu đủ emoji; lưu được rồi vẫn có thể lệch giới hạn index. Kiểm tra migration, đừng chỉ nhìn frontend.

4. “Word count cho tiếng Việt”
Tách theo space không phản ánh “số từ” ngôn ngữ Việt. Đừng dùng word count tool cho brief viết tiếng Việt có dấu cách bất thường.


Tóm tắt mang đi

  1. Surrogate pair = lý do emoji làm .length = 2
  2. Ba tầng: code unit → code point → grapheme; thêm UTF-8 byte khi nói payload
  3. Thống nhất đơn vị với backend trước khi viết if (length > N)
  4. Tool local giúp nhìn cùng lúc .length và byte — mở Đếm ký tự khi review PR có validation chuỗi

Bookmark bài này khi lần sau QA bảo “chữ ít mà vẫn over limit”: mở console, gõ s.length[...s].length, đối chiếu — thường ra đáp án trong 30 giây.