[EffectiveJavaScript輪読会]文字列は16ビットの符号単位を並べたシーケンスとして考えよう

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第8回目です。

前回のおさらい

前回は、「セミコロン挿入の限度を学ぼう」をお届けしました。

[EffectiveJavaScript輪読会]セミコロン挿入の限度を学ぼう
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第7回目です。前回のおさらい前回は、「型が異なるとき...

今回は、「文字列は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ビットの符号単位を並べたシーケンスとして考えよう

タイトルとURLをコピーしました