こうこく
作 ▸
改 ▸

charCodeAt と codePointAt の違い

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

[2020-04-05追記] この記事は2バイト使うサロゲートペア文字について触れていますが、肌の色が異なる絵文字や、家族が合体した絵文字の話をカバーできていません。

そのうちちゃんと調べたら書き直す予定ですが、この記事の内容が全てではないことに注意してください。以下の記事が参考になりました。

Unicode 絵文字にまつわるあれこれ (絵文字の標準とプログラム上でのハンドリング) - Qiita

取り急ぎですみません。

本題

例として『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進表記です。

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

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

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

Unicodeについて


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

ECMAScript® 2018 Language Specification

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

また、サロゲートペア文字であることがわかるのは、文字の1バイト目を見たときです。2バイト目だけ見ても、それがサロゲートペア文字であることはわかりません。

i = 4 のとき、charCodeAt() はサロゲートペア文字の上位1バイトだけのコード値 55356 を返しますが、codePointAt() はこれがサロゲートペア文字であると判断し、文字丸ごとのコードポイント 127813 を返します。

i = 5 のとき、charCodeAt() はサロゲートペア文字の下位1バイトだけのコード値 57157 を返します。codePointAt() も、i = 5 だけ見てもサロゲートペア文字だとはわからないので、同様にコードポイント 57157 を返します。

同じことが i = 6i = 7 のときも言えます。

codePointAt() はサロゲートペア文字でも単一のコード値が取得できて便利ですが、「一文字ずつ処理したい」という観点では、単純に length でループするだけではダメなようです。


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

文字列を1文字ずつ配列化(サロゲートペアを考慮) - Qiita

コード
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]
この記事に何かあればこちらまで (非公開)