【JavaScript】 文字列データの内部形式と関連メソッドについてまとめてみた
更新日:2020/12/16
JavaScriptの文字列は内部でどのような形で格納されているのだろうか?
少し気になったので調べてみました。
内部文字コードはUTF-16
JavaScriptの文字列は、内部的にUTF-16という形式で格納されています。
UTF-16は符号化形式と呼ばれるもので、Unicodeという符号化文字集合を16ビット単位でエンコードしたものです。
符号化形式や符号化文字集合については、初歩的なことを別サイトで解説しているのでそちらをみてください。
UTF-8・16やShift-JISって何?文字コードについて説明してみる
例えば、次のような文字列を定義したとします。
const a ="!XYZあいアイ";
わかりにくいですが「あ」と「い」は全角。
それ以外は半角です。
コードを実行することで、文字列コードは内部的に次のように格納されます。
! | X | Y | Z | あ | い | ア | イ |
00 21 | 00 58 | 00 59 | 00 5A | 30 42 | 30 44 | FF 71 | FF 72 |
!XYZはASCIIコードの範囲で1バイトで表すことができますが、UTF-16形式では00を付加して2バイト(16ビット)で格納されています。
「あいアイ」はUnicodeで定義された番号をそのまま2バイトで格納されています。
読み込み時にUTF-16に変換される
htmlデータや外部jsデータは、UTF-8やShift-JISで保存されています。
UTF-16で保存されているケースはあまり多くありません。
そのためデータをブラウザが処理する際に、データの文字コードを判別してUTF-16に変換しています。
判別が正確におこなわれないと文字化けの原因になります。
判別は応答ヘッダーやhtmlのmetaタグでおこないます。
詳しくは別記事で解説しているのでそちらをみてください。
■【JavaScript】 文字化けするとき確認すること
ちなみにNode.jsはソースファイルをUTF-8のみ受け付け、UTF-8からUTF-16への変換を内部的におこなっています。
それ以外のコードは文字化けするので、注意が必要です。
サロゲートペアとコードポイントとコードユニット
Unicodeは16ビットで表現できない文字も定義されています。
例えば古い漢字や絵文字などです。
U+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 10 00 | 𡀀 | 𡀁 | 𡀂 | 𡀃 | 𡀄 | 𡀅 | 𡀆 | 𡀇 | 𡀈 | 𡀉 | 𡀊 | 𡀋 | 𡀌 | 𡀍 | 𡀎 | 𡀏 |
1 F6 40 | 🙀 | 🙁 | 🙂 | 🙃 | 🙄 | 🙅 | 🙆 | 🙇 | 🙈 | 🙉 | 🙊 | 🙋 | 🙌 | 🙍 | 🙎 | 🙏 |
これをUTF-16で扱うとしたら2バイトではたりません。
そこで2バイトを2つ使用してあらわします。
その際、次のような少し複雑な計算をします。
- 1 00 00(16進) を引く
「🙅」1 F6 45 - 1 00 00 = F6 45
- 2進数表記にする
F6 45 → 11110110 01000101
- 下位から10ビット目で分割。上位も10ビットになるように0を補完する
0000111101 1001000101
- 上位に110110。下位に110111を付加
1101100000111101 1101111001000101
- 16進数にする
1101100000111101 1101111001000101
→D83D DE45
これをコードしてまとめると、次のようになる。
0x10000より大きいユニコードのUTF-16変換
const code = 0x1F645;
const c1 = code - 0x10000;
const utf16height = (c1 >> 10) | 0xD800;
const utf16Low = (c1 & 0x3FF) | 0xDC00;
console.log(utf16height.toString(16),utf16Low.toString(16)); // 結果:d83d de45
これを元のコードに戻すには、逆のことをおこないます。
2バイトで一組のUTF-16をユニコードに変換
const unicode = (utf16height & 0x3FF) << 10
| (utf16Low & 0x3FF)
+ 0x10000;
console.log(unicode.toString(16));
UTF-16で一文字を2バイトであらわすとき、このペアをサロゲートペアと呼びます。さらに、最初の2バイトを上位サロゲート、2番目の2バイトを下位サロゲートと呼びます。
また、「あ」をあらわす0x3042や「🙅」をあらわす0x1F645などのUnicode上のコード番号をコードポイントと呼びます。
さらにUTF-16上の2バイトデータをコードユニットと呼びます。
「あ」は0x3042という一つのコードユニットで、「🙅」は0xD83Dと0xDE45の二つのコードユニットであらわすことができます。
なお、文字のコードユニットを確認できるツールを作成しました。
■UTF-16 文字コード確認ツール
利用してみてください。
JavaScriptでユニコードを文字列に変換する標準機能
JavaScriptで文字列を定義する場合、文字列リテラルなどを使用します。
const moji = "あいうえお";
あまり使用する機会がありませんが、いくつかの方法でUnicodeを指定して文字列を定義することができます。
文字列リテラルを使用
文字列リテラル内で、"\u"の後に4桁の16進数を続けるとUnicodeとして判断されます。
const moji = "\u0078\u0079\u007A\u3042\u3044\u3046";
console.log(moji); // xyzあいう
サロゲートペアについても、各ペアを順番に並べます。
const moji = "\uD83D\uDE45";
console.log(moji); // 🙅
ちなみに、"\u"の後に{ }で囲んだ16進数を続けると、2バイト以上のUnicodeをそのまま記述できます。
const moji = "\u{1F645}";
console.log(moji); // 🙅
String.fromCodePoint()メソッドを利用する
String.fromCodePoint()メソッドは、Unicodeを文字列に変換してくれます。
サロゲート対象のコードもそのまま記述できます。
// const moji = String.fromCodePoint(0x0078) + String.fromCodePoint(0x0079) + …
// ↓ 下記に変更
//
const moji = [0x0078,0x0079,0x007A,0x3042,0x3044,0x3046,0x1F645]
.map( m => String.fromCodePoint(m) ).join("");
console.log(moji); // xyzあいう🙅
String.fromCharCode()メソッドを利用する
String.fromCharCode()メソッドはString.fromCodePoint()メソッドに似ていますが、こちらはコードユニットを引数として指定します。
0xFFFF以下のUnicodeは、どちらを使用しても同じ結果となります。
それより大きいサロゲート対象のコードは、サロゲートペアで記述する必要があります。
// const moji = String.fromCharCode(0x0078) + String.fromCharCode(0x0079) + …
// ↓ 下記に変更
//
const moji = [0x0078,0x0079,0x007A,0x3042,0x3044,0x3046,0xD83D,0xDE45]
.map( m => String.fromCharCode(m) ).join("");
console.log(moji); // xyzあいう🙅
サロゲートペアを考慮した文字数の取得
文字列の文字数は、通常ならlenghtプロパティで確認できます。
const moji = "xyzあいう";
console.log(moji.length); // 6
しかしサロゲートペアは2文字としてカウントされてしまいます。
const moji = "xyzあいう🙅";
console.log(moji.length); // 7ではなく8
絵文字や一部の旧漢字などの特殊な文字を含んでいる可能性があるなら、lengthプロパティでは正確な文字数を取得できません。
そこでサロゲートペアを考慮した文字数の取得方法をいくつか挙げてみます。
for-ofでカウントする
文字列に対してfor-of文を使用すると、コードポイント単位で文字が取り出されます。
const moji = "xyzあいう🙅";
let count=0;
for( const cp of moji) ++count;
console.log( count ); // 7
文字単位の配列に変換する
スプレッド構文を使用してコードポイント単位の配列要素に分割し、要素数を参照することで文字数を取得します。
const moji = "xyzあいう🙅";
console.log( [...moji].length ); // 7
JavaScriptで文字列からユニコードを取得する標準機能
こちらもあまり使用する機会がありませんが、いくつかの方法で文字列からUnicodeを取得することができます。
String.prototype.codePointAt()を使用する
String.prototype.codePointAt()は、文字列の指定した文字位置のコードポイント値を取得するメソッドです。
const moji = "x🙅yzあいう".codePointAt(1);
console.log(moji.toString(16)); // 1f645
上のコードは一見うまくいっているように見えます。
しかしcodePointAt()メソッドの引数はサロゲートを考慮した文字位置ではありません。
あくまでコードユニット単位での文字位置です。
そのため、上の例をcodePointAt(2)に変更して実行すると、次の"y"のコードではなくて"🙅"の下位サロゲートが取得されます。
codePointAt()メソッドのアルゴリズムを確認すると、文字位置のコードユニットが上位サロゲートだったら下位も含めてコードポイントに変換し、それ以外だったらそのまま返す、という処理をおこなっているのがわかります。
次の表は、例として挙げている文字列の文字位置とコードユニットの関係をあらわしてています。
文字位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
文字 | x | 🙅 | y | z | あ | い | う | |
コード ユニット | 0078 | D83D | DE45 | 0079 | 007A | 3042 | 3044 | 3046 |
この文字列にcodePointAt(1)を実行したとします。
文字位置1は上位サロゲートなので、後に続く下位サロゲートと共にコードポイントに変換して返します。
次にcodePointAt(2)を実行したとします。
文字位置2は上位サロゲートではないので、そのまま文字位置2のコードユニット値をコードポイントとして返します。
このことから、文字列全体をコードポイントを取得したい場合、次のように直接codePointAt()でのループでは正しい結果を得ることができません。
const moji = "x🙅yzあいう";
let count=0, cp="";
const unibuf=[];
while( (cp = moji.codePointAt(count)) !== undefined ){ // 範囲外を指定するとundefineが返る
unibuf.push(cp.toString(16));
++count;
}
console.log(unibuf.join(",")); // 結果:78,1f645,de45,79,7a,3042,3044,3046
上の結果で2番目の1f645は"🙅"のコードユニット値。3番目のde45は、"🙅"の下位サロゲートです。余計なデータが混ざっているので、期待していた結果ではありません。
文字列全体をコードポイントに変換するには、文字数の取得で紹介したfor-of文やスプレッド構文を利用します。
const moji = "x🙅yzあいう";
// for-of文を使用
const unibuf=[];
for( let cp of moji){
unibuf.push(cp.codePointAt(0).toString(16));
}
console.log(unibuf.join(",")); // 78,1f645,79,7a,3042,3044,3046
// スプレッド構文を使用
console.log([...moji]
.map( m => m.codePointAt(0).toString(16) )
.join(",")); // 78,1f645,79,7a,3042,3044,3046
String.prototype.charCodeAt()を使用する
String.prototype.charCodeAt()は、指定した位置の文字列からコードユニットを取得します。
サロゲートは考慮されません。
そのため、charCodeAt()の返り値を見てループ処理を行えます。
const moji = "x🙅yzあいう";
let count=0, cp="";
const unibuf=[];
while( !isNaN(cp = moji.charCodeAt(count)) ){ // 範囲外を指定するとNaNが返る
unibuf.push(cp.toString(16));
++count;
}
console.log(unibuf.join(",")); // 78,d83d,de45,79,7a,3042,3044,3046
更新日:2020/12/16
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。