JavaScript | |

charCodeAt と codePointAt の違い

文字をコード値に変換する charCodeAt()codePointAt() の違いがよく分かってませんでした。「codePointAt() 使ったほうがいいらしい」程度の認識だったので、ちゃんと調べてみました。

例として『ABあい🍅🐤!』という文字列があるとします。文字数は見ての通り7文字です。

この文字列を length ぶんループ処理しながら、ひとつずつ charCodeAt()codePointAt() に通してみます。

コード
var str = 'ABあい🍅🐤!';
var codes = [];
var points = [];
for (var i = 0; i < str.length; ++i) {
	codes.push(str.charCodeAt(i));
	points.push(str.codePointAt(i));
}
console.log('charCodeAt => ', codes);
console.log('codePointAt => ', points);
実行結果 (少し見やすく整えてます)
charCodeAt() =>  [65, 66, 12354, 12356,  55356, 57157,  55357, 56356, 33]
codePointAt() => [65, 66, 12354, 12356, 127813, 57157, 128036, 56356, 33]

charCodeAt()codePointAt() で結果が異なってます。また、結果の要素数から、実際のところABあい🍅🐤!』の length は9であったことが分かります。

なぜこうなるのか、無い頭しぼって図を描いてみました。実行結果と見比べてみてください。

『コード値』と言ってるものの内訳
『コード値』と言ってるものの内訳

そもそも……。

  • charCodeAt() が返却するのはUTF-16コードでした。
  • UTF-16コードは、ある文字列をUTF-16で表現した場合のコード値です。UTF-16は、16ビットのコードを使ってUnicode文字を表現するエンコード方式です。
  • UTF-16コードは 0 ~ 65,535 (0x0 ~ 0xFFFF) の範囲をとります。

一方で……。

  • codePointAt() が返却するのはUnicodeコードポイントでした。
  • Unicodeコードポイントは、純粋にUnicodeで表現できる文字ひとつひとつに振られている番号です。
  • Unicodeコードポイントは 0 ~ ‭1,114,111‬ (0x0 ~ 0x10FFFF‭) の範囲をとります。
  • Unicodeコードポイントの 0 ~ 65,535 は、UTF-16コードと同じ文字を指してます。

すると、UTF-16コードが65,536個しかないのに対して、Unicodeで表現したい文字は1,114,112個もあることになります。なので実際には、ひとつのUTF-16コードで表現できない文字については、UTF-16コードを2つ使うことで表現してるそうです。いわゆるサロゲートペアです。

先ほどの図でサロゲートペアを説明したのが次の図となります。サロゲートペア的にはコード値が16進数のほうがキリがいいので、こちらは16進表記です。

サロゲートペア
サロゲートペア

ここでは絵文字しか出てきませんが、絵文字以外にもサロゲートペア使用文字は漢字とか色々あります。漢字では𩸽 (ほっけ) が有名みたいですね。

このあたり全般の理解については、下記のサイト様が大変参考になりました。


で、JavaScriptの内部的には、文字列はUTF-16で扱われてるらしいです。これは、元を辿っていったらECMAScriptの仕様書に書いてありました。

つまり文字列の length がUTF-16コードの数とイコールになるのは、そもそもJavaScriptが文字列をUTF-16で扱ってるからです。『ABあい🍅🐤!』の length が9だったのは、🍅🐤 がサロゲートペアを使わないと表現できない (UTF-16コードが2つ必要な) 文字だったせいです。

ここでもう一度、最初のコードの実行結果を表にして見てみます。

i =012345678
charCodeAt()656612354123565535657157553575635633
codePointAt()65661235412356127813571571280365635633

もう分かりました。i = 4i = 6 のときに charCodeAt()codePointAt() で結果が異なるのは、charCodeAt() はサロゲートペア文字の上位16ビットだけを返してるのに対し、codePointAt() は文字丸ごとのコードポイントを返してるからです。

こうして見ると、codePointAt() はサロゲートペア文字でも単一のコード値が取得できて便利ですが、一文字ずつ処理するには単純に length でループするだけではダメなようです。上記の例では、i = 5i = 7 の値が余計になってしまってます。


じゃあ『ABあい🍅🐤!』を本当に7文字として処理するにはどうすればいいの!? という点については、ネットで調べると素晴らしい記事がいっぱい出くるので、そちらにお任せします。

コード
var chars = 'ABあい🍅🐤!'.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || [];
var points = [];
for (var i = 0; i < chars.length; ++i) {
	points.push(chars[i].codePointAt(0));
}
console.log('codePointAt() => ', points);
実行結果
codePointAt() => [65, 66, 12354, 12356, 127813, 128036, 33]