2022年10月18日

Unicodeと文字列内部構造

高度な知識

このセクションでは、文字列の内部構造をより深く掘り下げます。絵文字、珍しい数学記号や象形文字、その他の珍しい記号を扱う予定がある場合、この知識は役立ちます。

既に知っているように、JavaScriptの文字列はUnicodeに基づいています。各文字は1~4バイトのバイトシーケンスで表されます。

JavaScriptでは、16進数のUnicodeコードを指定して、次の3つの表記のいずれかを使用して文字を文字列に挿入できます。

  • \xXX

    XX00からFFまでの値を持つ2桁の16進数でなければなりません。その場合、\xXXはUnicodeコードがXXである文字です。

    \xXX表記は2桁の16進数しかサポートしていないため、最初の256個のUnicode文字に対してのみ使用できます。

    この最初の256文字には、ラテンアルファベット、最も基本的な構文文字、その他いくつかが含まれています。たとえば、"\x7A""z"(Unicode U+007A)と同じです。

    alert( "\x7A" ); // z
    alert( "\xA9" ); // ©, the copyright symbol
  • \uXXXX XXXX0000からFFFFまでの値を持つ正確に4桁の16進数でなければなりません。その場合、\uXXXXはUnicodeコードがXXXXである文字です。

    U+FFFFより大きいUnicode値を持つ文字もこの表記で表すことができますが、この場合はサロゲートペアと呼ばれるものを使用する必要があります(サロゲートペアについては、この章の後半で説明します)。

    alert( "\u00A9" ); // ©, the same as \xA9, using the 4-digit hex notation
    alert( "\u044F" ); // я, the Cyrillic alphabet letter
    alert( "\u2191" ); // ↑, the arrow up symbol
  • \u{X…XXXXXX}

    X…XXXXXXは、0から10FFFF(Unicodeで定義されている最高のコードポイント)までの1~6バイトの16進数値でなければなりません。この表記を使用すると、既存のすべてのUnicode文字を簡単に表すことができます。

    alert( "\u{20331}" ); // 佫, a rare Chinese character (long Unicode)
    alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long Unicode)

サロゲートペア

頻繁に使用される文字はすべて、2バイトコード(4桁の16進数)を持っています。ほとんどのヨーロッパ言語の文字、数字、および基本的な統一CJK統合漢字(CJK - 中国語、日本語、韓国語の表記体系から)は、2バイト表現を持っています。

当初、JavaScriptはUTF-16エンコーディングに基づいており、文字ごとに2バイトしか許可されていませんでした。しかし、2バイトでは65536個の組み合わせしか許可されず、Unicodeのすべての可能な記号には不十分です。

そのため、2バイト以上を必要とする珍しい記号は、「サロゲートペア」と呼ばれる2バイトの文字のペアでエンコードされます。

副作用として、このような記号の長さは2になります。

alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X
alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY
alert( '𩷶'.length ); // 2, a rare Chinese character

これは、JavaScriptの作成時にはサロゲートペアが存在せず、そのため言語によって正しく処理されないためです!

上記の文字列にはそれぞれ1つの記号がありますが、lengthプロパティは長さ2を示しています。

記号を取得することもトリッキーな場合があります。これは、ほとんどの言語機能がサロゲートペアを2つの文字として扱うためです。

たとえば、ここでは出力に2つの奇妙な文字が表示されます。

alert( '𝒳'[0] ); // shows strange symbols...
alert( '𝒳'[1] ); // ...pieces of the surrogate pair

サロゲートペアの部分は、互い nélkül意味を持ちません。そのため、上記の例のアラートは実際にはゴミを表示します。

技術的には、サロゲートペアはコードでも検出できます。文字のコードが0xd800..0xdbffの範囲にある場合、それはサロゲートペアの最初の部分です。次の文字(2番目の部分)のコードは0xdc00..0xdfffの範囲内にある必要があります。これらの範囲は、標準によってサロゲートペア専用に予約されています。

そのため、String.fromCodePointstr.codePointAtというメソッドがJavaScriptに追加され、サロゲートペアを処理できるようになりました。

これらは本質的にString.fromCharCodestr.charCodeAtと同じですが、サロゲートペアを正しく処理します。

違いはここにあります。

// charCodeAt is not surrogate-pair aware, so it gives codes for the 1st part of 𝒳:

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835

// codePointAt is surrogate-pair aware
alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, reads both parts of the surrogate pair

つまり、位置1から取得する場合(ここではむしろ正しくありません)、どちらもペアの2番目の部分のみを返します。

alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3
alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3
// meaningless 2nd half of the pair

サロゲートペアの処理方法の詳細については、後の章反復可能オブジェクトで説明します。おそらく、そのため特別なライブラリもありますが、ここで推奨できるほど有名なものは何もありません。

要点:任意の場所で文字列を分割するのは危険です

文字列を任意の位置で分割することはできません。たとえば、str.slice(0, 4)を取り、それが有効な文字列であると期待することはできません。

alert( 'hi 😂'.slice(0, 4) ); //  hi [?]

ここでは、出力にゴミ文字(スマイリーサロゲートペアの前半)が表示されます。

サロゲートペアを確実に操作する予定がある場合は、注意してください。大きな問題ではないかもしれませんが、少なくとも何が起こっているのかを理解する必要があります。

発音区別符号と正規化

多くの言語には、基本文字の上に/下にマークが付いた複合文字があります。

たとえば、文字aはこれらの文字の基本文字になる可能性があります:àáâäãåā

最も一般的な「複合」文字には、Unicodeテーブルに独自のコードがあります。しかし、可能な組み合わせが多すぎるため、すべてではありません。

任意の合成をサポートするために、Unicode標準では、複数のUnicode文字を使用できます。基本文字の後に、それを「装飾する」1つ以上の「マーク」文字が続きます。

たとえば、Sの後に特別な「上点」文字(コード\u0307)を付けると、Ṡのように表示されます。

alert( 'S\u0307' ); // Ṡ

文字の上に(または下に)別のマークが必要な場合でも問題ありません。必要なマーク文字を追加するだけです。

たとえば、「下点」文字(コード\u0323)を追加すると、「上点と下点が付いたS」になります:Ṩ

例:

alert( 'S\u0307\u0323' ); // Ṩ

これは大きな柔軟性を提供しますが、興味深い問題も発生します。2つの文字は視覚的には同じように見える場合がありますが、異なるUnicode合成で表される場合があります。

例:

let s1 = 'S\u0307\u0323'; // Ṩ, S + dot above + dot below
let s2 = 'S\u0323\u0307'; // Ṩ, S + dot below + dot above

alert( `s1: ${s1}, s2: ${s2}` );

alert( s1 == s2 ); // false though the characters look identical (?!)

これを解決するために、「Unicode正規化」アルゴリズムがあり、各文字列を単一の「標準」形式にします。

これはstr.normalize()によって実装されています。

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

面白いことに、私たちの状況では、normalize()は実際には3文字のシーケンスを1つにまとめます:\u1e68(2つの点が付いたS)。

alert( "S\u0307\u0323".normalize().length ); // 1

alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

実際には、常にそうとは限りません。その理由は、記号は「十分に一般的」であるため、Unicodeの作成者はそれをメインテーブルに含め、コードを与えたためです。

正規化ルールとバリアントの詳細については、Unicode標準の付録に記載されています。Unicode正規化形式しかし、ほとんどの実用的な目的のために、このセクションの情報で十分です。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関するご提案がありましたら、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 少数のコードを挿入するには<code>タグを使用し、複数の行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。