[JavaScript] Unicode 文字列やバイナリデータを Base64 エンコードおよびデコードする

JavaScript には Base64 エンコードするための `btoa` 関数が用意されていますが、挙動が少し特殊で、単純に Unicode 文字列を渡すとエラーが発生し、また、`ArrayBuffer` や `TypedArray` を直接渡すことができません。

そのため、Unicode 文字列やバイナリデータを `btoa` 関数で扱える状態に変換してから `btoa` 関数で Base64 エンコードします。

参考「[btoa() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/btoa)」

デコードを行う `atob` 関数に関しても同様の逆の変換が必要です。

参考「[atob() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/atob)」

# 1. バイナリデータの場合

Unicode 文字列も結果的にバイナリデータとして扱うため、バイナリデータを Base64 エンコードする方法を先に説明します。

## 1.1. Base64 エンコード

Base64 エンコード を行う `btoa` 関数は文字列を引数として渡しますが、その**文字列の文字 1 つ 1 つを 1 バイトのデータとみなします** (JavaScript の文字列の文字自体は 2 バイトです) 。

そのため、例えば `Uint8Array` であればその各要素を各文字に割り当てた文字列に変換すれば `btoa` 関数に渡すことができます (1 バイトずつに 1 バイトのデータが入っている状態から 2 バイトずつに 1 バイトのデータが入っている状態に変換する) 。

文字コードを文字として扱うために `String.fromCharCode` を用いて変換します。

```js:バイナリデータを Base64 エンコード
const decodeBinaryString = uint8Array => uint8Array.reduce(
	(binaryString, uint8) => binaryString + String.fromCharCode(uint8),
	'',
);

// 
const uint8ArrayA = new Uint8Array([0x00, 0x10, 0x20, 0x30]);
console.log(uint8ArrayA.toString());

const binaryStringA = decodeBinaryString(uint8ArrayA);
const base64 = btoa(binaryStringA);
console.log(base64);
```

```:実行結果
0,16,32,48
ABAgMA==
```

`ArrayBuffer` のデータを Base64 エンコードしたい場合は `Uint8Array` に変換します。

```js
const arrayBufferA = new Uint16Array([0x1000, 0x3020]).buffer;
const uint8ArrayA = new Uint8Array(arrayBufferA);
console.log(uint8ArrayA.toString());
```

## 1.2. Base64 デコード

Base64 デコードする場合は文字として扱っているバイトを本来のバイト列に戻します (2 バイトずつに 1 バイトのデータが入っている状態から 1 バイトずつに 1 バイトのデータが入っている状態に変換する) 。

文字を文字コードに変換するために `String.prototype.charCodeAt` を使用します。

Base64 デコードは `atob` 関数で行います。

```js:バイナリデータを Base64 デコード
const encodeBinaryString = binaryString => Uint8Array.from(
	binaryString,
	binaryChar => binaryChar.charCodeAt(0),
);

// 
const base64 = 'ABAgMA==';
console.log(base64);

const binaryStringB = atob(base64);
const uint8ArrayB = encodeBinaryString(binaryStringB);
console.log(uint8ArrayB.toString());
```

```:実行結果
ABAgMA==
0,16,32,48
```

参考「[TypedArray.from() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/from)」

# 2. Unicode 文字列の場合

## 2.1. Base64 エンコード

前述の通り JavaScript で扱う文字列の文字は 2 バイトのため、`btoa` 関数の仕様から、日本語等を含む Unicode 文字列を直接 `btoa` 関数に渡すとエラーが発生します。

`TextEncoder.prototype.encode` を用いて `Uint8Array` に変換することで、Unicode 文字列をバイナリデータとして Base64 エンコードすることができます。

```js:Unicode 文字列を Base64 エンコード
const textEncoder = new TextEncoder();
const encodeString = string => textEncoder.encode(string);

const decodeBinaryString = uint8Array => uint8Array.reduce(
	(binaryString, uint8) => binaryString + String.fromCharCode(uint8),
	'',
);

// 
const stringA = 'Unicode 文字列';
console.log(stringA);

const uint8ArrayA = encodeString(stringA);
const binaryStringA = decodeBinaryString(uint8ArrayA);
const base64 = btoa(binaryStringA);
console.log(base64);
```

```:実行結果
Unicode 文字列
VW5pY29kZSDmloflrZfliJc=
```

## 2.2. Base64 デコード

Base64 デコードする場合は `TextDecoder.prototype.decode` を用いて `Uint8Array` から Unicode 文字列に戻します。

```js:Unicode 文字列を Base64 デコード
const textDecoder = new TextDecoder();
const decodeString = buffer => textDecoder.decode(buffer);

const encodeBinaryString = binaryString => Uint8Array.from(
	binaryString,
	binaryChar => binaryChar.charCodeAt(0),
);

// 
const base64 = 'VW5pY29kZSDmloflrZfliJc=';
console.log(base64);

const binaryStringB = atob(base64);
const uint8ArrayB = encodeBinaryString(binaryStringB);
const stringB = decodeString(uint8ArrayB);
console.log(stringB);
```

```:実行結果
VW5pY29kZSDmloflrZfliJc=
Unicode 文字列
```

# 3. `escape` および `unescape` を用いる方法は非推奨

Unicode 文字列を扱う場合の `btoa` 関数のエラー対策として `escape` および `unescape` を用いる方法がありますが、`escape` および `unescape` 自体が非推奨になっているため使用しない方が良いです。

(ちなみに実行結果自体は前述の方法と一致するため、「エンコード結果が異なること」が非推奨な理由ではないです。)

```js:Unicode 文字列を Base64 エンコードおよびデコード ※非推奨のやり方
const stringA = 'Unicode 文字列';
console.log(stringA);

const binaryStringA = unescape(encodeURIComponent(stringA));
const base64 = btoa(binaryStringA);
console.log(base64);

const binaryStringB = atob(base64);
const stringB = decodeURIComponent(escape(binaryStringB));
console.log(stringB);
```

```:実行結果
Unicode 文字列
VW5pY29kZSDmloflrZfliJc=
Unicode 文字列
```

参考「[解決策その 1 - 文字列をエンコードする前にエスケープする - Base64 - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN](https://developer.mozilla.org/ja/docs/Glossary/Base64#%E8%A7%A3%E6%B1%BA%E7%AD%96%E3%81%9D%E3%81%AE_1_-_%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E5%89%8D%E3%81%AB%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E3%81%99%E3%82%8B)」
参考「[escape() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/escape)」
参考「[unescape() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/unescape)」