前端 Base64 編碼知識,一文打盡,探索起源,追求真相

大綱

方便移動端閱讀:

Base64 在前端的應用

Base64 編碼,你一定知道的,先來看看她在前端的一些常見應用:
當然絕部分場景都是基於 Data URLs[1]

Canvas 圖片生成

canvas 的 toDataURL[2] 可以把 canvas 的畫布內容轉 base64 編碼格式包含圖片展示的 data URI[3]。

const ctx = canvasEl.getContext("2d");
// ...... other code
const dataUrl = canvasEl.toDataURL();

// data:image/png;base64,iVBORw0KGgoAAAANSUhE.........

你畫我猜,新用戶加入,要獲取當前的最新的繪畫界面,也可以通過 Base64 格式的消息傳遞。

文件讀取

FileReader 的 readAsDataURL[4] 可以把上傳的文件轉爲 base64 格式的 data URI,比較常見的場景是用戶頭像的剪裁和上傳。

function readAsDataURL() {
    const fileEl = document.getElementById("inputFile");
    return new Promise((resolve, reject) ={
        const fd = new FileReader();
        fd.readAsDataURL(fileEl.files[0]);
        fd.onload = function () {
            resolve(fd.result);
            // data:image/png;base64,iVBORw0KGgoAAAA.......
        }
        fd.onerror = reject;
    });
}

jwt

jwt 由 header, payload,signature 三部分組成,前兩個解碼後,都是可以明文看見的。拿 國服最強 JWT 生成 Token 做登錄校驗講解,看完保證你學會![5] 裏面的 token 做測試。

網站圖片和小圖片

移動端網站圖標優化

<link rel="icon" href="data:," />
<link rel="icon" href="data:;base64,=" />

至於怎麼獲得這個值data:,的:

<canvas height="0" width="0" id="canvas"></canvas>
<script>
    const canvasEl = document.getElementById("canvas");
    const ctx = canvasEl.getContext("2d");
    dataUrl = canvasEl.toDataURL();
    console.log(dataUrl);  // data:,
</script>

小圖片

這個就有很多場景了,比如 img 標籤,背景圖等

img 標籤:

<img src="data:image/png;base64,iVBORw0KGgoAAAA......." />

css 背景圖:

.bg{
    background: url(data:image/png;base64,iVBORw0KGgoAAAA.......)
}

簡單的數據加密

當然這不是好方法,但是至少讓你不好解讀。

  const username = document.getElementById("username").vlaue; 
  const password = document.getElementById("password").vlaue;  
  const secureKey = "%%S%$%DS)_sdsdj_66";
  const sPass = utf8_to_base64(password + secureKey);
  
  doLogin({
      username,
      password: sPass
  })

SourceMap

借用阮大神的一段代碼, 注意 mappings 字段,這實際上就是 bas64 編碼格式的內容,當然你直接去解,是會失敗的。

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js""bar.js"],
    names: ["src""maps""are""fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
  }

具體的實現請看官方的 base64-vlq.js[6] 文件。

混淆加密代碼

著名的代碼混淆庫, javascript-obfuscator[7],其也是有應用 base64 幾碼的,一起看看選項:
webpack-obfuscator[8] 也是基於其封裝的。

    --string-array-indexes-type '<list>' (comma separated) [hexadecimal-number, hexadecimal-numeric-string]
    --string-array-encoding '<list>' (comma separated) [none, base64, rc4]
    --string-array-index-shift <boolean>
    --string-array-wrappers-count <number>
    --string-array-wrappers-chained-calls <boolean>

其他

X.509 公鑰證書, github SSH key, mht 文件,郵件附件等等,都有 Base64 的影子。

Base64 數據編碼起源

早期郵件傳輸協議基於 ASCII 文本,對於諸如圖片、視頻等二進制文件處理並不好。ASCII 主要用於顯示現代英文,到目前爲止只定義了 128 個字符,包含控制字符和可顯示字符。爲了解決上述問題,Base64 編碼順勢而生。

Base64 是編解碼,主要的作用不在於安全性,而在於讓內容能在各個網關間無錯的傳輸,這纔是 Base64 編碼的核心作用。

除了 Base64 數據編碼,其實還有 Base32 數據編碼, Base16 數據編碼,可以參見 RFC 4648[9]

Base64 編碼 64 的含義

64 就是 64 個字符的意思。

base64 對照表, 借用 Base64 原理 [10] 的一張圖:

  1. A-Z  26

  2. a-z  26

  3. 0-9  10

  4. + /  2

26 + 26 + 10 + 2  = 64

當然還有一個字符=,這是填充字符,後面會提到,不屬於 64 裏面的範疇。

對照表的索引值,注意一下,後面的 base64 編碼和解碼會用到。

Base64 編碼優缺點

優點

  1. 可以將二進制數據(比如圖片)轉化爲可打印字符,方便傳輸數據

  2. 對數據進行簡單的加密,肉眼是安全的

  3. 如果是在 html 或者 css 處理圖片,可以減少 http 請求

缺點

  1. 內容編碼後體積變大, 至少 1/3
    因爲是三字節變成四個字節,當只有一個字節的時候,也至少會變成三個字節。

  2. 編碼和解碼需要額外工作量


說完優缺點,回到正題:

我們今天的重點是 uf8 編碼轉 Base64 編碼:

基本流程

char => 碼點 => utf-8 編碼 => base64 編碼

在之前要解一下編碼的知識, 瞭解編碼知識,又要先了解一些計算機的基礎知識。

一些計算機和前端基礎知識

比特和字節

比特又叫位。在計算機的世界裏,信息的表示方式只有 0 和 1, 其可以表示兩種狀態。
一位二進制可以表示兩狀態, N 位可以表示2^N種狀態。

一個字節(Byte)有 8 位(Bit)

所以一個字節可以表示 2^8 = 256 種狀態;

獲得字符的 Unicode 碼點

String.prototype.charCodeAt[11] 可以獲取字符的碼點,獲取範圍爲0 ~ 65535。這個地方注意一下,關係到後面的 utf-8 字節數。

"a".charCodeAt(0)  // 97
"中".charCodeAt(0) // 20013

進製表示

  1. 0b開頭,可以表示二進制 注意0b10000000= 128 ,0b11000000=92,之後會用到.
0b11111111 // 255
0b10000000 // 128 後面會用到
0b11000000 // 192 後面會用到

  1. 0x開頭,可以表示 16 進制
0x11111111 // 286331153

0o開頭可以表示 8 進制,就不多說了,本來不會涉及。

進制轉換

10 進制轉其他進制
Number.prototype.toString(radix)[12] 可以把十進制轉爲其他進制。

100..toString(2)  // 1100100
100..toString(16) // 64, 也等於 ox64

其他進制轉爲 10 進制
parseInt(string, radix)[13] 可以把其他進制,轉爲 10 進制。

parseInt("10000000", 2) // 128
parseInt("10",16) // 16

這裏額外提一下一元操作符號+可以把字符串轉爲數字,後面也會用到, 之前提到的0b,0o,0x這裏都會生效。

+"1000" // 1000
+"0b10000000" // 128
+"0o10" // 8
+"0x10" // 16

位移操作

本文只涉及右移操作,就只講右移,右移相當於除以 2,如果是整數,簡單說是去低位,移動幾位去掉幾位,其實和 10 進制除以 10 是一樣的。

64 >> 2 = 16我們一起看一下過程

0 1 0 0 0 0 0 0       64
-------------------
   0 1 0 0 0 0 | 0 0  16

一元 & 操作和 一元|操作

一元 &

當兩者皆爲 1 的時候,值爲 1。本文的作用可用來去高位, 具體看代碼。
3553 & 36 = 0b110111100001 & 0b111111 = 100001
因爲高位缺失,不可能都爲 1,故均爲 0,  而低位相當於複製一遍而已

110111 100001
       111111
------------
000000 100001

一元 |
當任意一個爲 1,就輸出爲 1.  本文用來填補0。比如,把 3 補成 8 位二進制
3 | 256 = 11 | 100000000 = 100000011

100000011.substring(1)是不是就等於 8 位二進制呢00000011

具備了這些基本知識,我們就開始先了解編碼相關的知識。

ASCII 碼, Unicode , UTF-8

ASCII 碼

ASCII 碼第一位始終是 0, 那麼實際可以表示的狀態是 2^7 = 128 種狀態。

ASCII 主要用於顯示現代英文,到目前爲止只定義了 128 個字符,包含控制字符和可顯示字符。

完整的 ASCII 碼對應表,可以參見 基本 ASCII 碼和擴展 ASCII 碼 [14]

接下來是 Unicode 和 UTF-8 編碼,請先記住這個重要的知識:

Unicode

Unicode 爲世界上所有字符都分配了一個唯一的編號 (碼點),這個編號範圍從 0x000000 到 0x10FFFF (十六進制),有 100 多萬,每個字符都有一個唯一的 Unicode 編號,這個編號一般寫成 16 進制,在前面加上 U+。例如:的 Unicode 是 U+6398。

Unicode 有平面的概念,這裏就不拓展了。

Unicode 只規定了每個字符的碼點,到底用什麼樣的字節序表示這個碼點,就涉及到編碼方法。

UTF-8

UTF-8 是互聯網使用最多的一種 Unicode 的實現方式。還有 UTF-16(字符用兩個字節或四個字節表示)和 UTF-32(字符用四個字節表示)等實現方式。

UTF-8 是它是一種變長的編碼方式, 使用的字節個數從 1 到 4 個不等,最新的應該不止 4 個, 這個 1-4 不等,是後面編碼和解碼的關鍵。

UTF-8 的編碼規則:

  1. 對於只有一個字節的符號,字節的第一位設爲0,後面 7 位爲這個符號的 Unicode 碼。此時,對於英語字母 UTF-8 編碼和 ASCII 碼是相同的。

  2. 對於 n 字節的符號(n > 1),第一個字節的前 n 位都設爲 1,第 n + 1 位設爲0,後面字節的前兩位一律設爲 10。剩下的沒有提及的二進制位,全部爲這個符號的 Unicode 碼,如下表所示:

beKJEZ

我們可能沒見過字節數爲2或者爲4的字符, 字節數爲2的可以去 Unicode 對應表 [15] 這裏找,而等於4的可以去這看看 Unicode® 13.0 Versioned Charts Index[16]

下面這些碼點都處於0000 0080 ~ 0000 07FF, utf-8 編碼需要 2 個字節

下面這些碼點都處於0001 0000 ~ 0010 FFFF, utf-8 編碼需要 4 個字節

可能這裏光說不好理解,我們分別以英文字符a和中文字符來講解一下:

爲了驗證結果,可以去 Convert UTF8 to Binary Bits - Online UTF8 Tools[17]

英文字符a

  1. 先獲得其碼點,"a".charCodeAt(0) 等於 97

  2. 對照表格, 0~127, 需1個字節

  3. 97..toString(2) 得到編碼 1100001

  4. 根據格式0xxxxxxx進行填充, 最終結果

01100001

中文字符

  1. 先獲得其碼點,"掘".charCodeAt(0) 等於 25496

  2. 對照表格,2048 ~ 65535 需3個字節

  3. 25496..toString(2) 得到編碼 110 001110 011000

  4. 根絕格式1110xxxx 10xxxxxx 10xxxxxx進行填充, 最終結果如下

11100110 10001110 10011000

Convert UTF8 to Binary Bits - Online UTF8 Tools[18] 執行結果:完全匹配

抽象把字符轉爲 utf8 格式二進制的方法

基於上面的表格和轉換過程,我們抽象一個方法,這個方法在之後的 Base64 編碼和解碼至關重要

先看看功能,覆蓋 utf8 編碼 1-3 字節範圍

console.log(to_binary("A"))  // 11100001
console.log(to_binary("س"))  // 1101100010110011
console.log(to_binary("掘")) // 111001101000111010011000

方法如下

function to_binary(str) {
  const string = str.replace(/\r\n/g, "\n");
  let result = "";
  let code;
  for (var n = 0; n < string.length; n++) {
    //獲取麻點
    code = str.charCodeAt(n);
    if (code < 0x007F) { // 1個字節
      // 0000 0000 ~ 0000 007F  0 ~ 127 1個字節
      
      // (code | 0b100000000).toString(2).slice(1)
      result += (code).toString(2).padStart(8, '0'); 
    } else if ((code > 0x0080) && (code < 0x07FF)) {
      // 0000 0080 ~ 0000 07FF 128 ~ 2047 2個字節
      // 0x0080 的二進制爲 10000000 ,8位,所以大於0x0080的,至少有8位
      // 格式 110xxxxx 10xxxxxx     

      // 高位 110xxxxx
      result += ((code >> 6) | 0b11000000).toString(2);
      // 低位 10xxxxxx
      result += ((code & 0b111111) | 0b10000000).toString(2);
    } else if (code > 0x0800 && code < 0xFFFF) {
      // 0000 0800 ~ 0000 FFFF 2048 ~ 65535 3個字節
      // 0x0800的二進制爲 1000 00000000,12位,所以大於0x0800的,至少有12位
      // 格式 1110xxxx 10xxxxxx 10xxxxxx

      // 最高位 1110xxxx
      result += ((code >> 12) | 0b11100000).toString(2);  
      // 第二位 10xxxxxx
      result += (((code >> 6) & 0b111111) | 0b10000000).toString(2);
      // 第三位 10xxxxxx
      result += ((code & 0b111111) | 0b10000000).toString(2);
    } else {
      // 0001 0000 ~ 0010 FFFF   65536 ~ 1114111   4個字節 
      // https://www.unicode.org/charts/PDF/Unicode-13.0/U130-2F800.pdf
      throw new TypeError("暫不支持碼點大於65535的字符")
    }
  }
  return result;
}

方法中有三個地方稍微難理解一點,我們一起來解讀一下:

  1. **二字節 (code >> 6) | 0b11000000**其作用是生成高位二進制。
    我們以實際的一個栗子來講解,以س爲例,其碼點爲0x633, 在0000 0080 ~ 0000 07FF之間,佔兩個字節, 在其二進制編碼爲11 000110011 , 其填充格式如下, 低位要用 6 位
110xxxxx 10xxxxxx

爲了方便觀察,我們把 11 000110011 重新調整一下 11000 110011

(code>> 6) 等於 00110011 >> 6,右移 6 位, 直接幹掉低 6 位。爲什麼是 6 呢,因爲低位需要 6 位,右移動 6 位後,剩下的就是用於高位操作的位了。

11000000
   11000 | 110011 
--------------
11011000
  1. 二字節 (code & 0b111111) | 0b10000000
    作用,用於生成低位二進制。以س爲例,11000 110011, 填充格式
  110xxxxx 10xxxxxx

(code & 0b111111)這步的操作是爲了幹掉6位以上的高位,僅僅保留低6位。一元&符號,兩邊都是 1 的時候纔會是 1,妙啊。

11000 110011
      111111
------------------
      110011

接着進行 | 0b10000000, 主要是按照格式10xxxxxx進行位數填補, 讓其滿 8 位。

 11000 110011
       111111         (code & 0b111111)
 ------------------
       110011  
    10 000000         (code & 0b111111) | 0b10000000
-------------------
    10 110011

Base64 編碼和解碼

utf-8 轉 Base64 編碼規則

  1. 獲取每個字符的 Unicode 碼,轉爲 utf-8 編碼

  2. 三個字節作爲一組,一共是 24 個二進制位
    字節數不能被 3 整除,用 0 字節值在末尾補足

  3. 按照 6 個比特位一組分組,前兩位補 0,湊齊 8 位

  4. 計算每個分組的數值

  5. 以第4步的值作爲索引,去 ASCII 碼錶找對應的值

  6. 替換第2添加字節數個數=
    比如第2添加了 2 個字節,後面是 2 個=

以大掘A爲例, 我們通過上面的utf8_to_binary方法得到 utf8 的編碼
11100110 10001110 10011000 11000001, 其字節數不能被 3 整除,後面填補

11100110
10001110
10011000
01000001
--------
00000000
00000000

6 位一組分爲四組, 高位補0, 用| 分割一下填補的。

00 | 111001  =57 =5
00 | 101000  =40 => o
00 | 111010  =58 =6
00 | 011000  =24 => Y

00 | 110000  =16 => Q
00 | 010000  =16 => Q
00 | 000000  =>    ==
00 | 000000  =>    ==

結果是:5o6YQQ==, 完美。

utf-8 轉 Base64 編碼規則代碼實現

基於上面的to_binary方法和 base64 的轉換規則,就很簡單啦:
先看看執行效果,very good, 和 base64.us[19] 結果完全一致。

console.log(utf8_to_base64("a")); // YQ==

console.log(utf8_to_base64("Ȃ"));  // yII=

console.log(utf8_to_base64("中國人")); // 5Lit5Zu95Lq6

console.log(utf8_to_base64("Coding Writing 好文召集令|後端、大前端雙賽道投稿,2萬元獎池等你挑戰!"));
//Q29kaW5nIFdyaXRpbmcg5aW95paH5Y+s6ZuG5Luk772c5ZCO56uv44CB5aSn5YmN56uv5Y+M6LWb6YGT5oqV56i/77yMMuS4h+WFg+WlluaxoOetieS9oOaMkeaImO+8gQ==

完整代碼如下:

const BASE64_CHARTS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function utf8_to_base64(str: string) {
  let binaryStr = to_binary(str);
  const len = binaryStr.length;

  // 需要填補的=的數量
  let paddingCharLen = len % 24 !== 0 ? (24 - len % 24) / 8 : 0;

  //6個一組
  const groups = [];
  for (let i = 0; i < binaryStr.length; i += 6) {
    let g = binaryStr.slice(i, i + 6);
    if (g.length < 6) {
      g = g.padEnd(6, "0");
    }
    groups.push(g);
  }

  // 求值
  let base64Str = groups.reduce((b64str, cur) ={
    b64str += BASE64_CHARTS[+`0b${cur}`]
    return b64str
  }"");

  // 填充=
  if (paddingCharLen > 0) {
    base64Str += paddingCharLen > 1 ? "==" : "=";
  }

  return base64Str;
}

至於解碼,是其逆過程,留給大家去實現吧。

其他的成熟方案

  1. 當然是基於已有的 btoaatob, 但是 unescape 是不被推薦使用的方法
function utf8_to_b64( str ) {
  return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
  return decodeURIComponent(escape(window.atob( str )));
}

// Usage: utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU=" b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

  1. MDN 的 [rewriting atob() and btoa() using TypedArrays and UTF-8](https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_2_%E2%80%93_rewriting_atob_and_btoa_using_typedarrays_and_utf-8 "Permalink to Solution #2 – rewriting atob("rewriting atob() and btoa() using TypedArrays and UTF-8") and btoa() using TypedArrays and UTF-8") 其支持到 6 字節,但是可讀性並不好。

  2. 第三方庫 base64-js[20] 與 js-base64[21] 都是周下載量過百萬的庫。

雖然有那麼多成熟的,但是我們理解和自己實現,才能更明白 Base64 的編碼原理。

額外補充一點

  1. 編碼關係圖 借用 [你真的瞭解 Unicode 和 UTF-8 嗎?[22]] 一張圖:

  1. DOMString[23] 是utf-16編碼

寫在最後

添加我的微信 dirge-cloud,帶帶我,一起學習。

引用

Version-Specific Charts[24]
Unicode13.0.0[25]
Unicode® 13.0 Versioned Charts Index[26]
RFC 4648  | The Base16, Base32, and Base64 Data Encodings[27]
Base64 encoding and decoding[28]
字符編碼筆記:ASCII,Unicode 和 UTF-8[29]
Unicode 與 JavaScript 詳解 [30]
Base64 編碼入門教程 [31]Base64 原理 [32]
詳解 base64 原理 [33]
一文讀懂 base64 編碼 [34]
JS 中關於 base64 的一些事 [35]
Base64 的原理、實現及應用 [36]
圖片與 Base64 換算關係 [37]
[你真的瞭解 Unicode 和 UTF-8 嗎?[38]]
Unicode 中 UTF-8 與 UTF-16 編碼詳解 [39]
Unicode 對應表 [40]
JavaScript Source Map 詳解 [41]

參考資料

[1] Data URLs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs

[2] toDataURL: https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toDataURL

[3] data URI: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs

[4] readAsDataURL: https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsDataURL

[5] 國服最強 JWT 生成 Token 做登錄校驗講解,看完保證你學會!: https://blog.csdn.net/u011277123/article/details/78918390

[6] base64-vlq.js: https://github.com/mozilla/source-map/blob/master/lib/base64-vlq.js

[7] javascript-obfuscator: https://github.com/javascript-obfuscator/javascript-obfuscator#javascript-obfuscator-options

[8] webpack-obfuscator: https://github.com/javascript-obfuscator/webpack-obfuscator

[9] RFC 4648: https://datatracker.ietf.org/doc/html/rfc4648

[10] Base64 原理: https://juejin.cn/post/6844903698045370376

[11] String.prototype.charCodeAt: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt

[12] Number.prototype.toString(radix): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString

[13] parseInt(string, radix): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt

[14] 基本 ASCII 碼和擴展 ASCII 碼: https://www.asciim.cn/

[15] Unicode 對應表: http://titus.uni-frankfurt.de/unicode/unitestx.htm

[16] Unicode® 13.0 Versioned Charts Index: https://www.unicode.org/charts/PDF/Unicode-13.0/

[17] Convert UTF8 to Binary Bits - Online UTF8 Tools: https://onlineutf8tools.com/convert-utf8-to-binary

[18] Convert UTF8 to Binary Bits - Online UTF8 Tools: https://onlineutf8tools.com/convert-utf8-to-binary

[19] base64.us: https://base64.us/

[20] base64-js: https://www.npmjs.com/package/base64-js

[21] js-base64: https://www.npmjs.com/package/js-base64

[22] [你真的瞭解 Unicode 和 UTF-8 嗎?: https://www.cnblogs.com/reycg-blog/p/10021658.html

[23] DOMString: https://developer.mozilla.org/zh-CN/docs/Web/API/DOMString

[24] Version-Specific Charts: https://www.unicode.org/charts/About.html

[25] Unicode13.0.0: https://www.unicode.org/versions/Unicode13.0.0

[26] Unicode® 13.0 Versioned Charts Index: https://www.unicode.org/charts/PDF/Unicode-13.0/

[27] RFC 4648  | The Base16, Base32, and Base64 Data Encodings: https://datatracker.ietf.org/doc/html/rfc4648

[28] Base64 encoding and decoding: https://developer.mozilla.org/en-US/docs/Glossary/Base64

[29] 字符編碼筆記:ASCII,Unicode 和 UTF-8: http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html?20110621174302

[30] Unicode 與 JavaScript 詳解: http://www.ruanyifeng.com/blog/2014/12/unicode.html

[31] Base64 編碼入門教程: https://juejin.cn/post/6898104998547161096

[32] Base64 原理: https://juejin.cn/post/6844903698045370376

[33] 詳解 base64 原理: https://juejin.cn/post/6946169611461066789

[34] 一文讀懂 base64 編碼: https://juejin.cn/post/6844904197519835150

[35] JS 中關於 base64 的一些事: https://juejin.cn/post/6844903838286102536

[36] Base64 的原理、實現及應用: https://juejin.cn/post/6844903663459106829

[37] 圖片與 Base64 換算關係: https://juejin.cn/post/6913814643618086925

[38] [你真的瞭解 Unicode 和 UTF-8 嗎?: https://www.cnblogs.com/reycg-blog/p/10021658.html

[39] Unicode 中 UTF-8 與 UTF-16 編碼詳解: https://juejin.cn/post/6844903590155272199

[40] Unicode 對應表: http://titus.uni-frankfurt.de/unicode/unitestx.htm

[41] JavaScript Source Map 詳解: https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Xc4v21d5qcbpMcSZF-N6AQ