Loại bỏ dấu tiếng Việt trong chuỗi Javascript ES6

Một vấn đề chúng ta thường gặp khi làm việc với chuỗi trong javascript đó là phải so sánh các chuỗi có dấu và không dấu tương đồng (ở các ngôn ngữ có dấu như tiếng Việt hay tiếng Tây Ban Nha,…).

Chẳng hạn khi viết hàm tìm kiếm, nếu không loại bỏ dấu của chuỗi chúng ta sẽ không thể nào với từ khóa ‘the thao’ tìm được chuỗi có chứa từ ‘thể thao’ hoặc ngược lại.

Trước đây, cách phổ biến là liệt kê hết các trường hợp kí tự có dấu và thay thế chúng bằng kí tự latin không dấu tương ứng. Ví dụ mình hay viết một hàm như sau để xóa dấu tiếng Việt cho string:

function removeAccents(str) {
  var AccentsMap = [
    "aàảãáạăằẳẵắặâầẩẫấậ",
    "AÀẢÃÁẠĂẰẲẴẮẶÂẦẨẪẤẬ",
    "dđ", "DĐ",
    "eèẻẽéẹêềểễếệ",
    "EÈẺẼÉẸÊỀỂỄẾỆ",
    "iìỉĩíị",
    "IÌỈĨÍỊ",
    "oòỏõóọôồổỗốộơờởỡớợ",
    "OÒỎÕÓỌÔỒỔỖỐỘƠỜỞỠỚỢ",
    "uùủũúụưừửữứự",
    "UÙỦŨÚỤƯỪỬỮỨỰ",
    "yỳỷỹýỵ",
    "YỲỶỸÝỴ"    
  ];
  for (var i=0; i<AccentsMap.length; i++) {
    var re = new RegExp('[' + AccentsMap[i].substr(1) + ']', 'g');
    var char = AccentsMap[i][0];
    str = str.replace(re, char);
  }
  return str;
}

Có nhiều cách khác để làm điều tương tự. Nhìn chung là hoạt động khá ổn và chính xác. Tuy nhiên do phải liệt kê hết các trường hợp có dấu nên khá mất công, chưa kể các trường hợp hoa/thường cũng phải phân biệt rõ ràng khiến code dài dòng hơn.

Với ES6, chúng ta có thể làm việc này nhanh gọn hơn chỉ với 1 dòng code nhờ vào method String.prototype.normalize.

Normalize, NFC, NFD là gì?

normalize() là phương thức mới trong ES6 dùng để trả chuỗi về một dạng chuẩn hóa unicode. Cú pháp của method này như sau:

str.normalize([form])

// Trong đó:
// + str: Chuỗi cần xử lý
// + [form]: Dạng chuẩn hóa
//   - Mặc định: NFC
//   - Các giá trị có thể nhận: NFC, NFD, NFKC, NFKD

Bảng mã Unicode cho chúng ta một không gian mã cực lớn để lưu trữ các loại kí tự, kí hiệu, dấu,… Điều này cũng dẫn đến một chuyện đó là bạn có khả năng thể hiện một kí tự theo nhiều cách khác nhau. Ví dụ:

var str1 = '\u00EA';
// Kí tự Latin e với dấu mũ ở trên đầu

var str2 = '\u0065\u0302';
// Kí tự Latin e + Dấu mũ kết hợp

Rõ ràng str1 == str2false. Tuy nhiên hai chuỗi trên lại cùng thể hiện chữ cái “ê” trong tiếng Việt. Vậy nên nếu muốn so sánh tương đồng thì trước tiên bạn phải chuẩn hóa chúng về cùng một dạng.

Hai dạng chuẩn hóa chính là NFC (Chuẩn hóa form C) và NFD (Chuẩn hóa form D). NFC là dạng chuẩn hóa kết hợp, có nghĩa là các kí tự kết hợp dấu sẽ được giữ nguyên, ví dụ chữ ê sẽ sử dụng điểm mã u00EA map về glyph Latin Small Letter E with Circumflex. Còn NFD là dạng chuẩn hóa phân tách, có nghĩa là các kí tự có dấu nếu phân tách được sẽ khai triển hết ra thành các điểm mã map về các glyph thành phần. Ví dụ so sánh giữa 2 dạng:

NFCnghiêng
Code pointu006Eu0067u0068u0069u00EAu006Eu0067
NFDnghie◌̂ng
Code pointu006Eu0067u0068u0069u0065u0302u006Eu0067

Vậy còn NFKC, NFKD?

Trong bảng mã Unicode, bên cạnh các kí tự tiêu chuẩn còn có các kí tự trông tương tự để sử dụng cho các mục đích khác nhau. Ví dụ Mathematical Alphanumeric Symbols là bảng các bộ chữ cái, chữ số Latin hoặc Hy Lạp có style khác giúp cho việc biểu diễn các khái niệm, biến,… trong toán học, vật lý,…

NFKC và NFKD là hai kiểu chuẩn hóa tương thích. Chúng hoạt động tương tự như NFC và NFD nhưng ngoài ra còn chuẩn hóa cả những kí tự tương tự như trên về kí tự tiêu chuẩn. (Bạn hãy thử copy 𝓤𝓷𝓲𝓬𝓸𝓭𝓮 vào Google Search và sẽ thấy vẫn ra kết quả về unicode, đó là nhờ chuẩn hóa tương thích)

Xóa dấu với Normalize

Quay lại câu chuyện bỏ dấu tiếng Việt hay bất kì ngôn ngữ có dấu nào khác. Có lẽ đến đây bạn đã nắm được tinh thần của phương pháp này rồi. Đó là chúng ta sẽ chuẩn hóa các chuỗi Unicode về dạng NFD nhằm tách dấu và chữ cái ra, sau đó xóa hết các kí tự thể hiện dấu.

Thực hiện điều này khá đơn giản vì các block chứa kí tự dấu kết hợp có range từ u0300 đến u036f trong bảng mã Unicode. Vậy nên chúng ta chỉ cần viết:

function removeAccents(str) {
  return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}

Nhưng. Còn một vấn đề sẽ xảy ra, đó là có một số kí tự mà chúng ta coi là biến thể kết hợp ở bên ngoài nhưng lại là kí tự độc lập ở trong bảng mã Unicode và không (hoặc chưa) tách ra được. Tạm thời bỏ qua các ngôn ngữ khác, trong tiếng Việt có một trường hợp cụ thể là chữ cái “đ”. Chúng ta vẫn hay coi đd có thêm gạch ngang, nhưng normalize sẽ không đưa đ về d + gạch ngang mà vẫn giữ nguyên.

Vậy nên để hoàn thiện hàm bỏ dấu tiếng Việt, chúng ta sẽ thêm như sau:

function removeAccents(str) {
  return str.normalize('NFD')
            .replace(/[\u0300-\u036f]/g, '')
            .replace(/đ/g, 'd').replace(/Đ/g, 'D');
}

Nhược điểm

Thứ nhất là tương thích. Dù sao hiện tại ES6 vẫn chỉ là mostly supported chứ chưa phải fully supported. Tuy nhiên nếu bạn không quá bận tâm về tương thích ngược với các browser cũ thì không sao cả.

Nhược điểm thứ hai đó là nếu trong chuỗi của bạn có chứa kí tự thuộc dãy u0300-u036f đứng độc lập thì cũng sẽ bị xóa luôn. Tuy nhiên trường hợp này mình nghĩ là khá hi hữu.