JavaScript

JavaScriptのnormalize():Unicode正規化で文字列の比較をそろえる

この記事でわかること

String.prototype.normalize()で、見た目が同じ文字列の比較失敗を解消する方法を解説。正規化形式の選び方、ブラウザーとNode.jsの実行手順、検索への応用と制約を学びます。

検索対象に「ガ」があるのに、入力した「ガ」と一致しない。こうした現象は、文字列を構成するUnicodeの表現が違うときに起こります。「ガ」には、1文字の表現と、「カ」に結合用の濁点を続ける表現があるためです。

String.prototype.normalize()は、文字列を指定したUnicode正規化形式に変換するメソッドです。本記事では、比較する文字列の表現をそろえる使い方と、検索用データへ適用する際の判断を解説します。

normalize()の目的と基本仕様

JavaScriptの文字列比較は、画面に描かれた形を見て判断しません。同じように見えても、内部の文字列が異なれば===の結果はfalseになります。

normalize()を使うと、Unicodeで等価と定義された表現を共通の形式にそろえられます。ただし、あらゆる表記揺れを吸収する機能ではありません。例えば、ひらがなとカタカナを同じ文字に変換する用途には別の処理が必要です。

引数と戻り値

呼び出し方はtext.normalize(form)です。textが変換対象で、引数formに正規化形式を指定します。

  • "NFC":正準等価性に基づいて分解し、可能なものを再合成します。
  • "NFD":正準等価性に基づいて分解します。
  • "NFKC":互換等価性に基づいて分解し、正準等価性に基づいて再合成します。
  • "NFKD":互換等価性に基づいて分解します。

省略時とundefined指定時は"NFC"です。戻り値は正規化後の文字列で、元の文字列は変更されません。指定できない形式を渡すとRangeErrorが発生します。仕様の詳細はMDNのnormalize()リファレンスで確認できます。

実装時には、最初から4形式を使い分ける必要はありません。まず"NFC"で同じ文字の表現をそろえ、文字幅などの違いも検索で吸収したい場合に"NFKC"を検討すると、要件を整理しやすくなります。

動作環境と実行方法

2026年9月25日時点で確認したMDNでは、normalize()は広く利用可能な機能で、2016年9月から主要ブラウザーを横断して利用できるとされています。本記事のブラウザー実行対象は、更新されたChrome、Edge、Firefox、Safariです。古いブラウザーや独自の組み込み環境について、一律に動作を保証するものではありません。

Node.jsでも利用できます。本記事では公式配布のNode.js 24系を実行対象とします。これは本記事の対象バージョンであり、APIが24系で初めて追加されたという意味ではありません。Node.js 24.0.0の国際化ドキュメントにも対象機能として掲載されています。

ブラウザーで試す

空のタブなどで開発者ツールを開き、Consoleに次節のJavaScriptコード全体を入力して実行します。ページの要素を操作しないコードなので、専用のHTMLや外部ライブラリーは不要です。

コードを即時実行関数で囲んであるため、同じConsoleで繰り返し実行しても、トップレベルの変数の再宣言を避けられます。Consoleが最後に表示するundefinedは、ログとは別の評価結果です。

Node.jsで試す

次節のコードをUTF-8のnormalize-demo.jsに保存します。ターミナルでその保存先へ移動してから、以下を実行してください。

Shell
node --version
node normalize-demo.js

本記事の前提では、最初のコマンドがv24.で始まるバージョンを返すことを確認します。パッケージのインストールや通信は必要ありません。

ただし、Node.jsをICUなしで独自ビルドした場合、normalize()は変換しない関数になります。関数の存在だけでは判定できないため、以下のコードでは代表的な文字列が実際に正規化されるかも確認します。この制約はNode.jsの国際化サポートに記載されています。

自己完結したコードで比較を確認する

以下はブラウザーとNode.jsで共通のコードです。文字列の違いがコピー時にも明確になるよう、一部をUnicodeエスケープで記述しています。

JavaScript
(() => {
  if (
    typeof String.prototype.normalize !== "function" ||
    "\u30AB\u3099".normalize("NFC") !== "\u30AC"
  ) {
    console.error("Unicode正規化を利用できない環境です");
    return;
  }

  const composed = "\u30AC";
  const decomposed = "\u30AB\u3099";

  console.log(`正規化前: ${composed === decomposed}`);
  console.log(`元の長さ: ${composed.length}, ${decomposed.length}`);

  const left = composed.normalize("NFC");
  const right = decomposed.normalize("NFC");

  console.log(`NFCで比較: ${left === right}`);
  console.log(`NFC後の長さ: ${left.length}, ${right.length}`);
  console.log(`元データの長さ: ${decomposed.length}`);

  const wide = "ABC123";
  console.log(`NFCで文字幅を変換: ${wide.normalize("NFC") === "ABC123"}`);
  console.log(`NFKCの結果: ${wide.normalize("NFKC")}`);

  try {
    "ガ".normalize("nfc");
  } catch (error) {
    console.log(`形式の指定ミス: ${error.name}`);
  }
})();

対応環境での期待出力は次のとおりです。実機で採取したログではなく、仕様とコードから導いた結果です。

CODE
正規化前: false
元の長さ: 1, 2
NFCで比較: true
NFC後の長さ: 1, 1
元データの長さ: 2
NFCで文字幅を変換: false
NFKCの結果: ABC123
形式の指定ミス: RangeError

出力をどう読むか

最初の比較では、1文字の「ガ」と、2文字分の「カ+結合用濁点」が異なるため、結果はfalseです。両方を"NFC"へ変換すると、同じ表現になってtrueになります。

この例のlengthはUTF-16コード単位数です。一般に、人が見た文字の数をそのまま表すわけではありません。また、正規化後も元のdecomposedの長さは2のままです。変換結果を使うには、戻り値を変数に受け取るか、そのまま比較へ渡します。

"NFC"では全角英数字をASCIIへそろえません。例のような文字幅の違いを吸収するのは"NFKC"です。形式名は大文字で指定し、"nfc"は使用できません。

活用事例:表示を残して検索キーを作る

商品検索などで全角英数字の入力も受け付けたい場合、表示用の文字列を残し、比較専用のキーだけを正規化する設計が使えます。以下も単独で実行できるコードです。前節と同じ対応環境で試してください。

JavaScript
(() => {
  const products = [
    { id: "pen", label: "ABCペン" },
    { id: "notebook", label: "ABCノート" },
    { id: "case", label: "XYZケース" }
  ];

  const indexed = products.map((product) => ({
    ...product,
    searchKey: product.label.normalize("NFKC")
  }));

  const input = "ABC";
  const query = input.normalize("NFKC");
  const matches = indexed.filter((product) =>
    product.searchKey.includes(query)
  );

  console.log(matches.map((product) => product.label).join(" / "));
})();

期待出力は次のとおりです。

CODE
ABCペン / ABCノート

登録データと検索語の両方を同じ形式にすることが要点です。片方だけを変換すると、もう片方に残った表現の違いで一致しなくなります。結果の表示にはlabelを使うため、登録時の文字幅が保たれます。

この設計なら、商品データの読み込み時にsearchKeyを作り、検索のたびには検索語だけを変換できます。商品名を更新したときはキーも更新します。なお、この例は空文字で検索すると全件が一致するため、実際の画面で空入力をどう扱うかは別途決めてください。

失敗例と導入時の制約

NFKCを元データの上書きに使う

"NFKC"は全角英数字だけを対象とする処理ではありません。丸数字なども変換されるため、表示や識別に意味のある違いを失う可能性があります。検索キーに採用する前に、同一視してよい入力例を具体的に決めてください。

例えば、独立した商品コードが正規化後に同じ値になれば、キーの衝突が発生します。検索で候補を増やす用途と、データを一意に識別する用途は、それぞれ要件を確認する必要があります。

正規化だけで検索要件を満たそうとする

この検索例では大文字と小文字を区別します。また、前後の空白の除去、ひらがなとカタカナの統一、読み仮名による検索は行いません。追加の変換が必要なら、まず期待する入力と検索結果を例示してから処理を追加します。

normalize()は同期処理であり、awaitやPromiseは不要です。誤った形式名による例外も呼び出し時に発生します。外部データを受け取る処理では、呼び出す前に値が文字列かを確認してください。nullや数値に対する通常のメソッド呼び出しは失敗します。

導入時は、比較する双方へ同じ形式を適用し、変換結果を利用し、表示用の原文を保持するところから始めると確認しやすくなります。

公式参考資料

PREFERRED SOURCES

Googleの優先するニュース提供元にSEの部屋を追加

Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。

Googleの設定画面で確認する 新しいタブで開きます

候補にSEの部屋が表示されない場合は、まだ追加できません。

y.
WRITTEN BY

y_ymo10

SEの部屋で、HTML・CSSからJavaScript・React・Next.jsまで、Web制作の開発ノートを公開しています。

ほかの開発ノートを読む →