どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第8回目です。
前回のおさらい
前回は、「セミコロン挿入の限度を学ぼう」をお届けしました。
今回は、「文字列は16ビットの符号単位を並べたシーケンスとして考えよう」 をお届けします。
テキスト第1章「JavaScriptに慣れ親もう」の項目7に対応しています。
項目7で腹落ちすべきことは、本文にもある 「JavaScriptの文字列の要素は16ビットの符号単位である」 を理解することです。
問題は小さく切り分けましょう。以下の用語と親しくなればいいはずです。むずかしくありません。
今日のアジェンダ
- JavaScriptの文字列の要素
- 16ビット
- 符号単位
- 実務を意識するなら
JavaScriptの文字列の要素
文字列の要素
文字列とは「hello」のような、単語や文章、つまり 文字の集まり です。
文字列のひとつひとつの要素を、文字と呼びます。
文字列「hello」があったとして、文字は左から順番にならんでいます(アラビア語など一部の言語は右から)ので、インデックス(のようなもの)が振れます。
ということは、文字にインデックス(のようなもの)を振った表を作成して、みんなで共有すれば、誰でも間違いなく文字を呼び出せる気がします。
この、文字とインデックス(のようなもの)が書かれた対応表のことを、Unicode(ユニコード) と呼びます。
インデックス(のようなもの)とは、0から1,113,111の整数で、Unicode用語で コードポイント と呼ばれます。
「全世界のありとあらゆる文字に、コードポイントを割り当てて管理しよう」 というのが、人類と文字のあくなき戦いであり、Unicodeの歴史です。
エンコードとは
インデックス(のようなもの)が「コードポイント」と呼ばれる理由は、数学用語の符号(code:コード)から来ています。
符号は、もともと、しるしという意味ですが、文字列の位置にしるしをつける、というイメージです。
さて、符号は、「インデックス(ようなもの)」のままでは、パソコンが認識できません。そこで、符号をパソコンでも認識ができるように、整数ではなく、パソコン内の処理方法と同じく、ビットに変換 します。(1ビットは0と1で表記する方法でしたね)
この、文字をビットに変換することを、符号化(エンコード)と呼びます。
エンコードにはさまざまな種類がありますが、もっとも強い関心事は、「何ビットに変換したら効率がいいのか」 ということです。
1ビットは0か1かの2通りです。2ビットは00か01か10か11の4通りです。アルファベットでも24通り必要なわけですから、4ビット(16通り)でも足りません。
ビット数は少ない方が処理が早いですが、表現できる桁数も少ないです。ビット数は大きいに越したことはありませんが、1990年代当時には、64ビットを計算できるパソコンなどありませんでした。
そこで、JavaScriptは エンコードを16ビット(65536通り)で行う規格 を採用しました。
文字を16ビットで変換する規格は、UTF-16 と呼ばれています。
16ビット
ヘキサリテラル
少し、項目2の、数値リテラルの復習をしましょう。
数値の104(ひゃくよん)は、2進数であらわすと、1101000(いちいちぜろいちぜろぜろぜろ)です。16進数であらわすと、68(ろくはち)です。
数値の先頭に「0b(ぜろびー)」をつけることで、2進数に評価するバイナリリテラルがありましたね。
その他に、「0x(ぜろえっくす)」をつけることで、16進数に評価するヘキサリテラルもあります。
この、ヘキサリテラルをよく使っていくので、覚えておきましょう。
function myFunction1_7_01() {
//10進数から2進数や16進数への変換
console.log((104).toString(2)); //1101000
console.log((104).toString(16)); //68
//バイナリリテラルとヘキサリテラル
console.log(0b1101000); //104
console.log(0x0068); //104
}
fromCodePoint() メソッド
文字を16ビットで変換するということは、1つの文字に、0~65535内のユニークな値が振られるということです。
通常は、このように、ヘキサリテラルで表記されます。
JavaScriptには、コードポイントから、文字を特定する fromCodePoint()メソッド があります。
引数のコードポイントは、16進数でも10進数でもかまいません。
codePointAt()メソッド では、文字のコードポイントを10進数で調べることができます。
function myFunction1_7_02() {
//16進数で指定
console.log(String.fromCodePoint(0x0068, 0x0065, 0x006c, 0x006c, 0x006f, 0x0021)); //hello!
//10進数で指定
console.log(String.fromCodePoint(104, 101, 108, 108, 111, 33)); //hello!
//引数のインデックス位置にある文字の、コードポイントを10進数で返す
console.log('hello'.codePointAt(0)); //104
}
16ビットの限界
くどいようですが、文字を16ビットで変換するということは、1つの文字に、0~65535内のユニークな値 が振られるということでした。
Unicodeができた当初は、16ビット(65535通り)あれば十分であろうと考えられていました。コードポイントと16ビット表記の間は1対1の関係になり、処理コストも低かったから、なおさら支持されました。
しかし、絵文字や特殊文字の登場で、65535通りでは枯渇することが判明しました。65535以上のコードポイントを表現しなければなりません。
JavaScriptでは、どのようにこの課題をクリアしているのでしょうか。
function myFunction1_7_03() {
//65535を超えるコードポイントをもつ文字
console.log('𝄞'.codePointAt(0)); //119070 (ト音記号)
console.log('𪚥'.codePointAt(0)); //173733 (龍が4つ。読み:テツ)
}
符号単位(コードユニット)
UTF-16のように、文字をビット列にエンコードしたさい、要素の最小単位を、符号単位(コードユニット)と呼びます。
JavaScriptは、UTF-16を採用しています。UTF-16のコードユニットのサイズは、16ビットです。
さきほどの課題は、この、16ビットのコードユニットを2つ組み合わせて、1つのコードポイントを表現しよう という仕組みで解決しています。
これを、サロゲートペアと呼びます。
サロゲートペア
上記のような、「’𝄞 𪚥’」(ト音記号+スペース+テツ)は、文字数でいうと3つですが、コードユニットでいうと5つ です。
この裏を取ってみましょう。
JavaScriptのString.lengthプロパティは、文字数ではなく、コードユニット数 を返します。
また、charCodeAt()メソッド は、指定されたインデックスにあるUTF-16コードユニットを、10進数で返します。
念のため、16進数も確認してみましょう。
function myFunction1_7_04() {
//コードユニットを返すlengthプロパティ
console.log('𝄞 𪚥'.length); //5
//コードユニットを返すlengthプロパティ
console.log('𝄞 𪚥'.charCodeAt(0)); //55348
console.log('𝄞 𪚥'.charCodeAt(1)); //56606
console.log('𝄞 𪚥'.charCodeAt(2)); //32 ←スペースのコードユニット=コードポイント
//16進数を確認する
console.log((55348).toString(16)); //0xd834
console.log((56606).toString(16)); //0xdd1e
}
つまり、JavaScriptの文字は、内部的には、16ビットのコードユニットで管理されている と言えます。
すべては、JavaScriptが文字エンコードにUTF-16を採用しているから です。
サロゲートペアもUTF-16の仕様です。
そして、世界中の文字符号化形式がUTF-16で統一されていれば問題ありませんが、UTF-8やUTF-32を採用しているアプリケーションやサーバーもまだまだ多いです。
上記のト音記号(𝄞)をコピーして、Windowsのメモ帳に貼り付けてみてください。文字化けしてしまうと思います。
それは、メモ帳の文字エンコード方式がUTF-8 だからです。
文字化け問題にぶつかったときは「エンコード方式が違うんだな」と気楽に考えましょう。
実務を意識するなら
ちょっと、疲れたので、Shift-JISなどの日本語エンコードの深掘りまでできませんでした。
1つだけ。UTF-8だからサロゲートペアが使えないということでもありません。
みなさん大好き JSON文字列 は、UTF-8を用いてエンコードしなければなりませんが、UTF-16サロゲート・ペアをエンコードした12文字のシーケンスとして文字を表します。
たとえば、ト音記号は、「\ud834\udd1e」と表すことができます。
興味深いので、ぜひ写経してみてください。
function myFunction1_7_05() {
const json = '{"clef":"\ud834\udd1e" }';
const jsonObj = JSON.parse(json);
console.log(jsonObj.clef); //𝄞
}
まとめ
以上で、「文字列は16ビットの符号単位を並べたシーケンスとして考えよう」をお届けしました。
コードポイントは、連続して並んでいるので「シーケンス」とも呼ばれますが、シーケンスというのは「順序、順番、配列する」という意味 なので、とくにむずかしくとらえる必要はありません。
今回、はじめて文字コードと向き合いましたが、これでもまだ 文字コード沼の入口に立っただけ のような気がします。
第1回目の輪読会は、ここまでです。
また、輪読会が開催されるさいは、アウトプットしたいと思います。
参考資料
コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus) 西尾 泰和
プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識 日経ソフトウェア 矢沢久雄
JSON(JavaScript Object Notation)データ交換フォーマット
このシリーズの目次
[EffectiveJavaScript輪読会]ノンプロ研EffectiveJavaScript輪読会とは
[EffectiveJavaScript輪読会]どのJavaScriptをつかっているのかを意識しよう
[EffectiveJavaScript輪読会]JavaScriptの浮動小数点を理解しよう
[EffectiveJavaScript輪読会]暗黙の型変換に注意しよう
[EffectiveJavaScript輪読会]オブジェクトラッパーよりもプリミティブが好ましい
[EffectiveJavaScript輪読会]型が異なるときに==を使わない
[EffectiveJavaScript輪読会]セミコロン挿入の限度を学ぼう
[EffectiveJavaScript輪読会]文字列は16ビットの符号単位を並べたシーケンスとして考えよう