JavaScript

JavaScriptのisWellFormed():孤立サロゲートを検出して文字列の破損を見つける

この記事でわかること

String.prototype.isWellFormed()で不正なUTF-16文字列を検出する方法を解説。引数・戻り値、ブラウザーとNode.jsの実行手順、期待出力、入力検証の実例と判定の限界を学べます。

文字列を途中で切り出したあと、URL用に変換しようとすると例外が発生することがあります。原因の一つが、絵文字などを表すUTF-16のペアが分断され、片方だけ残ることです。

String.prototype.isWellFormed()は、このような孤立サロゲートが文字列に含まれていないかを確認するメソッドです。この記事では、文字列の破損を検出し、不正な入力を処理の入口で拒否する使い方を説明します。

何を検出できるのか

JavaScriptの文字列はUTF-16のコード単位で扱われます。日本語の「あ」は1コード単位ですが、絵文字の「😀」は2コード単位で表されます。この2つの組をサロゲートペアと呼びます。

ペアの前半だけ、または後半だけが残ったものが孤立サロゲートです。JavaScriptはその状態の文字列も保持できますが、外部向けの変換処理では問題になることがあります。

たとえば、"😀".slice(0, 1)は見た目の1文字を取り出す処理にはなりません。UTF-16の先頭1コード単位だけを残すため、ペアを壊します。

isWellFormed()は文字数を数えたり、文字列を書き換えたりせず、孤立サロゲートの有無を真偽値で返します。文字の構造についてはMDNのString解説に説明があります。

動作環境と対応バージョン

2026年11月8日時点で確認した公式資料では、次のバージョンから利用できます。

  • Chrome 111以降
  • Edge 111以降
  • Firefox 119以降
  • Safari 16.4以降
  • Node.js 20.0.0以降

ブラウザーの対応はweb.devの2023年10月の機能紹介、Node.jsへの導入はNode.js 20.0.0のリリースノートで確認できます。MDNでは2023年10月から主要ブラウザー間で利用可能な機能として案内されています。

このAPIはブラウザーとNode.jsの両方で動きます。DOMや外部パッケージは不要です。ただし、APIの導入バージョンと製品の保守期間は別です。新しく実行環境を用意するときは、保守中のNode.jsを選んでください。

未対応環境の確認

メソッドがない環境で直接呼び出すとTypeErrorになります。実行前には次のように確認できます。

JavaScript
console.log(typeof String.prototype.isWellFormed === "function");

対応環境ならtrueです。後述のサンプルはこの確認を組み込み、未対応環境では説明を表示して処理を終えます。機能がない場合に、検証を省略して正常扱いする設計は避けましょう。

引数・戻り値と基本構文

基本構文はtext.isWellFormed()です。検査する文字列が呼び出し元になり、引数はありません。

  • 孤立サロゲートがなければtrueを返します。
  • 孤立サロゲートが一つでもあればfalseを返します。
  • 空文字列もtrueです。
  • 元の文字列は変更しません。

不正な文字列そのものを理由に例外を投げるAPIではありません。入力が文字列であることを確認してから呼べば、判定結果に応じて拒否や通知を選べます。構文と戻り値はMDNのisWellFormed()リファレンスに記載されています。

型の検証も必要

nullや数値を受け取る可能性がある処理では、先にtypeof value === "string"を確認します。すべての値がこのメソッドを持つわけではありません。

先にString(value)で変換すると、nullは文字列の"null"になります。それを受理してよいかは業務上の判断です。ここでは、文字列以外を明確に拒否します。

コピーして動かす入力検証サンプル

次のコードは、通常の文字列、正しい絵文字、孤立サロゲート、文字列以外の入力を検査します。外部通信やファイル読み込みは行いません。

inspectText()は、この例で用意する独自関数です。isWellFormed()の結果を使い、表示用の判定文字列を返します。

JavaScript
(() => {
  "use strict";

  if (typeof String.prototype.isWellFormed !== "function") {
    console.log("この環境はisWellFormed()に対応していません");
    return;
  }

  function inspectText(value) {
    if (typeof value !== "string") {
      return "拒否: 文字列ではありません";
    }

    if (!value.isWellFormed()) {
      return "拒否: 孤立サロゲートがあります";
    }

    return "受理";
  }

  const samples = [
    { label: "日本語", value: "こんにちは" },
    { label: "絵文字", value: "\uD83D\uDE00" },
    { label: "空文字", value: "" },
    { label: "前半だけ", value: "\uD83D" },
    { label: "後半だけ", value: "\uDE00" },
    { label: "途中で切断", value: "\uD83D\uDE00".slice(0, 1) },
    { label: "数値", value: 123 },
  ];

  for (const { label, value } of samples) {
    console.log(`${label}: ${inspectText(value)}`);
  }
})();

ブラウザーでの実行方法

対応ブラウザーで空のタブを開き、開発者ツールのConsoleを選びます。上のJavaScriptコード全体を貼り付け、実行してください。HTMLファイルや開発サーバーを用意する必要はありません。

Node.jsでの実行方法

読者の手元でコードをwell-formed-demo.jsとしてUTF-8で保存します。ターミナルでそのファイルを保存したフォルダーへ移動し、次を実行してください。ライブラリのインストールやpackage.jsonの作成は不要です。

Shell
node --version
node well-formed-demo.js

期待出力

対応環境では、コードから次の7行の出力が導かれます。ここに示す結果は仕様に基づく期待値であり、実機検証済みの記録ではありません。

CODE
日本語: 受理
絵文字: 受理
空文字: 受理
前半だけ: 拒否: 孤立サロゲートがあります
後半だけ: 拒否: 孤立サロゲートがあります
途中で切断: 拒否: 孤立サロゲートがあります
数値: 拒否: 文字列ではありません

絵文字をUnicodeエスケープで記述しているのは、ペアの前半と後半をコード上で区別するためです。空文字の受理はAPIの仕様に合わせています。必須項目として使う場合は、空文字を拒否する条件を別途追加します。

失敗例:判定せずにURL用へ変換する

孤立サロゲートを含む文字列をencodeURIComponent()へ渡すとURIErrorが発生します。この条件はMDNのencodeURIComponent()リファレンスで確認できます。

次のコードは先ほどのサンプルとは独立して実行できます。同じ方法でConsoleへ貼り付けるか、別のJavaScriptファイルとしてNode.jsで実行してください。

JavaScript
(() => {
  "use strict";

  if (typeof String.prototype.isWellFormed !== "function") {
    console.log("この環境はisWellFormed()に対応していません");
    return;
  }

  const broken = "\uD83D\uDE00".slice(0, 1);

  try {
    encodeURIComponent(broken);
  } catch (error) {
    if (!(error instanceof URIError)) {
      throw error;
    }
    console.log(`直接変換: ${error.name}`);
  }

  if (!broken.isWellFormed()) {
    console.log("事前判定: 不正な文字列のため変換を中止");
  } else {
    console.log(encodeURIComponent(broken));
  }
})();

期待出力は次のとおりです。例外メッセージは実装によって異なるため、共通の例外名だけを表示しています。

CODE
直接変換: URIError
事前判定: 不正な文字列のため変換を中止

isWellFormed()自体は同期処理で、Promiseを返しません。awaitや非同期の例外処理は不要です。この例では、変換時の失敗と事前判定の違いを示すためにtry...catchを使っています。

判定の限界と実務での活用

正常判定でも内容の妥当性は保証しない

trueは孤立サロゲートがないことだけを意味します。文字化けした文章、制御文字、見た目が似た文字、不適切な入力も、孤立サロゲートがなければ通過します。

また、表示上の文字数、文字列の正規化、HTMLへの安全な出力、URLの許可判定は別の問題です。このAPIだけで入力検証を完結させないでください。

検査を置く場所を決める

活用しやすいのは、外部データの取り込み時や、独自の文字列加工を終えた直後です。たとえば、インポートした商品名を検査し、不正な行だけを修正対象として返す処理に使えます。

検査後にコード単位で切り出すと、正常だった文字列を再び壊す可能性があります。検査を入口に置くだけでなく、変換先へ渡す最終的な文字列にも適用する設計が有効です。

不正な入力を検出したら、元データの再取得や修正を促す方針を決めます。検出だけでは失われたペアを復元できません。利用者の入力を黙って変更しないことにも意味があります。

改善策として、拒否理由と対象レコードの識別子を返す形にすると、取り込み処理の修正作業につなげやすくなります。本文全体をログへ出さずに問題箇所を追えるため、運用上も扱いやすくなります。

公式参考資料

PREFERRED SOURCES

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

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

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

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

y.
WRITTEN BY

y_ymo10

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

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