js基石之字符ASCII,GBK,Unicode,utf-32,utf-16,utf-8,encodeuri,encodeuricomponent,base64

js基石之数据类型一:类型分类&区别
js基石之数据类型二:类型判断
js基石之数据类型三:类型转换
js基石之Number:本质
js基石之Number:应用(数字运算,数字&字符串转换,不同进制表示&相互转换)
js基石之字符: ASCII,GBK,Unicode,utf-32,utf-16,utf-8,encodeuri,encodeuricomponent,base64

先有问题再有答案

  1. 字符是什么?
  2. 字符'a'在计算机中是如何存在的?
  3. ASCII GBK Unicode 是什么 ?
  4. utf-32 utf-16 utf-8 是做什么的 ?
  5. '𠮷a'.length === 3 // true 为什么
  6. 如何可以使'𠮷a'的长度为2 ?
  7. "👨‍👩‍👧".length === 1 或者 "👨‍👩‍👧".length === 8 ?
  8. encodeuri,encodeuricomponent,base64 有什么用?

字符char

问题1,2 done.

在计算机中,字符是用来表示文本信息的基本单位。因为在计算机这个二进制的世界里 只有0和1,所以每个字符,无论是字母、数字、标点符号还是其他符号,最后都会使用一个数字来标识,这个数字也就是码点,再将码点转换为二进制数字,存储为一个或多个字节。

这个将字符转换为对应二进制数字的过程即为编码, 计算机底层的二进制码转换成屏幕上有意义的字符(如"hello world"),这个过程就称为解码.

js使用UTF-16的编码方式表示一个字符。也就是说一般情况下一个字符占两个字节

字符集

问题3 done

一个字符对应一个数字编号(码点),收录多个字符与码点的集合被称为字符集。字符集在计算机的发展过程中出现了很多。

ASCII: 重要意义

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)字符集是最早的计算机字符编码标准之一,于1963年制定。

在很长一段历史中,计算机仅仅应用在欧洲的一些发达国家,因此在他们的程序中只存在他们所理解的拉丁字母(如a、b、c、d等)和阿拉伯数字,他们在编码解码时也只需要考虑这一种情况,就是如何将这些字符转换成计算机所能理解的二进制数。

每个ASCII编码对应一个特定的字符。例如,大写字母A的ASCII编码是65,小写字母a的ASCII编码是97,数字1的ASCII编码是49。非打印字符,像回车,换行,制表符等,也有对应的ASCII编码。例如,换行符的ASCII编码是10。

ASCII在设计时,也考虑了方便计算机处理的问题。比如大写英文字母和其对应的小写英文字母之间的ASCII值,相差32,所以想要将小写字母转化为大写,或者反之,则只需进行简单的位运算即可。

ASCII 字符集支持 128 种字符,仅使用 7 个 bit 位,也就是一个字节的后 7 位就可以将它们全部表示出来,而最前面的一位统一规定为 0 即可(如 0110 0001 表示 a)。

后来,为了能够表示更多的欧洲国家的常用字符如法语中带符号的字符 é,又制定了 ASCII 额外扩展的版本 EASCII,这里就可以使用一个完整子节的 8 个 bit位表示共 256 个字符,其中就又包括了一些衍生的拉丁字母。

GBK:中国自己的字符集 适合中文环境

当计算机传入中国后,我们的汉字同样需要编码数字化,因为ASCII最多能表示256个字符,对用中国的汉字而言实在是太少了,于是我们就搞了自己的字符集。即GBK字符集

GBK 字符集收录了 2 万多个汉字,以及所有的 ASCII 字符,最重要的是所有 ASCII 字符的代码点在 GBK 字符集和 ASCII 字符集里都是一样的。 GBK 编码采用 2 个字节来表示一个汉字,但为了避免浪费空间,ASCII 字符只会占 1 个字节

解码时如何知道某个字节是在代表一个 ASCII 字符,还是需要再组合一个字节来代表一个汉字呢?GBK 编码采用了一种很巧妙的方式,即 2 字节的字符对应的二进制第一位永远是 1,这样解码的时候,如果遇到 1 开头的字节,就连读 2 个字节按一个字符进行解码,如果遇到 0 开头的字节,这知道这个是 ASCII 字符了,不需要再继续读一个字节放在一起解了。由于这种机制,GBK 实际上无法完全利用 2 的 16 次方 (65536) 大小的空间,因为 2 字节字符的第一位的 1 是固定的,可修改的 bit 只有 15 个,但 3 万多的空间,也足够放下大部分汉字了。

其他字符集

所以问题来了,计算机在中国需要一套编码,在韩国呐?日本呐?印度呐?世界上各国都有不同的编码方式,同一个二进制数字可以被解码成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为了解决这个问题,最终的集大成者 Unicode 字符集出现了。

Unicode:万国码

它成功实现了每个数字代表唯一的至少在某种语言中使用的符号,目前,有一百多万个(从 0x0000000x10FFFF),这么一来即使全世界所有语言的字符都放进来也还绰绰有余,甚至放了很多 emoji 😂 表情字符。

既然可以表示这么多字符,那岂不是每个字符也需要更大的空间,不然岂不是会重复?确实如此,需要 4 个字节来表示。但 Unicode 提供了多种字符编码方式,有些编码方式可以帮我们节省空间。

关于unicode我们需要知道的点有

  1. 收录了世界上的所有语言文字表示的字符,而且目前每年依然可以扩展
  2. 这么多的字符 最多使用32位可以完全覆盖 还有很多很多剩余空间....
  3. 依然兼容ASCII,即 0~127 意义依然不变

编码方式

问题4 done

一般一个字符集对应的编码方式是固定的,但是unicode并不是这样 它支持多种编码方式例如utf-32, utf-16, utf-8. utf原Unicode Transformation Format;

utf-32: 固定长度的编码方式。

每四个字节即32位代表一个字符,这意味着每个字符都使用相同数量的字节来表示。这使得字符的定位和计数变得非常简单,因为你只需要跳过固定数量的字节就可以找到下一个字符。这是 UTF-32 的主要优点。

UTF-32 的主要缺点是它的空间效率较低。由于每个字符都使用 4 个字节,所以对于那些可以使用更少字节表示的字符(例如 ASCII 字符),UTF-32 会浪费大量的空间。此外,由于每个字符都使用 4 个字节,所以 UTF-32 编码的文本在网络传输时可能会比其他编码方式更慢。

utf-16: js使用的编码方式

变长的编码方式, 使用 16 位(即 2 个字节)作为基本单位进行编码.

采用 UTF-16 编码的一个 Unicode 字符可能是 2 字节大小,也可能是 4 字节大小。具体来说,Unicode 中的前 65,536 个字符(也被称为基本多语言平面,包括大多数常用字符)可以使用一个 16 位单元来表示。超出这个范围的字符则需要使用两个 16 位单元(也被称为代理对)来表示。

utf-8: 网络中最常用的方式没有之一

即使是 UTF-16,每个字符最少也依然需要 2 个字节表示,这依然存在空间浪费。于是终极版的 UTF-8 编码它来了.

utf-8变长的编码方式,使用一个字节作为基本单位编码.

需要注意的是:

  1. 一个字节分配给ASCII 所以一个字节是 0xxxxxxx的格式
  2. 多个字节编码规则 开头有几个1表示有几个字节:
    二个字节110xxxxx 10xxxxxx
    三个字节1110xxxx 10xxxxxx 10xxxxxx
    四个字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

对js的影响

获取码点

unicode字符集中每个字符对应的数字编号即为码点,在js中可以通过codePointAt 和 charCodeAt 方法获取对应的码点。

charCodeAt :

这个方法返回字符串中指定位置字符的 UTF-16 码位。如果该字符是基本多语言平面(BMP)的字符(即码位小于或等于 U+FFFF 的字符),那么这个方法返回的就是该字符的 Unicode 码位。但是,如果该字符是辅助平面的字符(即码位大于 U+FFFF 的字符),那么这个方法只会返回该字符的第一个 16 位单元(也就是代理对的高代理项)。

arduino 复制代码
"好".charCodeAt() // 22909 

codePointAt

这个方法返回字符串中指定位置字符的完整 Unicode 码位。无论该字符是基本多语言平面的字符还是辅助平面的字符,这个方法都能正确返回其 Unicode 码位。

perl 复制代码
let str = '𠜎'; // 这个字符的 Unicode 码位是 U+2070E console.log(str.charCodeAt(0)); // 输出 55360,这是 '𠜎' 的高代理项 console.log(str.codePointAt(0)); // 输出 132878,这是 '𠜎' 的正确 Unicode 码位

码元&码点。

问题5 done

因为js使用utf-16的方式来表示一个字符,所以在字符的编码小于65,536的情况下 字符的长度即为1,当字符的编码大于这个长度后 utf-16使用两个16位 所以一个字符的长度会变为2.

arduino 复制代码
'好'.codePointAt() // 码点 22909 < 65536 
'好'.length === 1 // true

'𠮷'.codePointAt() // 码点 134071 > 65536 
'𠮷'.length === 2 // true

在js中2字节的基本单位也被称为码元,表现为字符串的长度为1,js中大部分方法都是使用码元的方式计算长度的。

perl 复制代码
"😄".split(""); //  ['\uD83D', '\uDE04'] 一个字符占据2个码元 因为split按照码元分拆 所以打印了unicode编码

想要正确的访问字符或者打印字符串的长度可以使用码点的方式,迭代器支持按照码点访问问题6 done

ini 复制代码
[..."😄"] // ['😄']
[..."😄"].length === 1 // true
[...'𠮷'].length === 1 // true

字素簇(grapheme cluster)& 零宽度连接符(ZWJ)

问题7 done

Unicode 除了使用单个码点表示 Emoji,还允许多个码点组合表示一个 Emoji。

其中的一种方式是"零宽度连接符"(ZERO WIDTH JOINER,缩写 ZWJ)U+200D。举例来说,下面是三个 Emoji 的码点。

  • U+1F468:男人
  • U+1F469:女人
  • U+1F467:女孩

上面三个码点使用U+200D连接起来,U+1F468 U+200D U+1F469 U+200D U+1F467,就会显示为一个 Emoji 👨‍👩‍👧,表示他们组成的家庭。如果用户的系统不支持这种方法,就还是显示为三个独立的 Emoji 👨👩👧。

javascript 复制代码
// 将多个unicode字符通过ZWJ连接一起
function joinWithZWJ(items) {
    // 将每个元素转换为字符
    let chars = items.map(item => {
        if (typeof item === 'string') {
            return item;
        } else if (typeof item === 'number') {
            return String.fromCodePoint(item);
        } else {
            throw new Error('Invalid input');
        }
    });

    // 使用零宽度连接符将字符连接起来
    let result = chars.join('\u200D');

    return result;
}

const family = joinWithZWJ(["👨","👩","👧"]) // family = "👨‍👩‍👧"

"👨‍👩‍👧".length === 8 // true length按照码元计算长度
[..."👨‍👩‍👧"].map(item=> `${item}`.codePointAt()) //  [128104, 8205, 128105, 8205, 128103]
// 8  =  3个码点 * 2 + 2(ZWJ)

// 正则判断输入的字符是否为通过zwj连接的
/\u{200D}/u.test('a👨‍👩‍👧') // true

前端常用的编码方式

问题8 done

encodeuri: 编码uri

  1. 特殊字符:URL 中有一些字符有特殊的含义,如问号(?)用于开始查询字符串,井号(#)用于开始片段标识符,斜杠(/)用于分隔路径的各个部分。如果你需要在 URL 的数据部分使用这些字符,你需要对它们进行编码,否则它们可能会被误解为它们的特殊含义。
  2. 非 ASCII 字符URL 必须是 ASCII 字符集的一个子集,这意味着所有的非 ASCII 字符 在 URL 中使用时都需要进行编码。
  3. 空格:URL 中不能直接包含空格,否则可能会导致错误。你需要使用 encodeURI 来将空格编码为 %20。
  4. 安全性:对 URL 进行编码可以防止注入攻击,因为它可以确保数据被正确地解析,而不是被误解为代码。
javascript 复制代码
encodeURI('https://example.com/search?q=你好/世界')
// 'https://example.com/search?q=%E4%BD%A0%E5%A5%BD/%E4%B8%96%E7%95%8C'

encodeuri的编码方式为% + urf-8的数字

encodeuricomponent: 编码uri参数

uri中特殊含义的字符也会被编码 所以不能用encodeuricomponent编码完整的uri

javascript 复制代码
`https://example.com/search?q=${encodeURIComponent('你好/世界')}`
// 'https://example.com/search?q=%E4%BD%A0%E5%A5%BD%2F%E4%B8%96%E7%95%8C'

base64: 编码ascii字符

btoa:将ascii字符串或二进制数据转换成一个base64编码过的字符串,该方法不能直接作用于Unicode字符串。 atob:将已经被base64编码过的数据进行解码。

csharp 复制代码
btoa('aa') // 'YWE='
btoa('世界') // 直接编码中文 报差错
VM520:1 Uncaught DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
    at <anonymous>:1:1

utf8字符串base64编码与解码:

scss 复制代码
function uft8ToBase64(utf8) {
return btoa(encodeURIComponent(utf8));
}
function base64ToUtf8(base64) {
return decodeURIComponent(atob(base64));
}
var base64 = uft8ToBase64("hello {世界} ");
base64ToUtf8(base64); // 'hello {世界} '

参考

  1. ruanyifeng_base64
  2. ruanyifeng_emoji
  3. full-emoji-list
  4. 一次性搞懂字符集和字符编码(ASCII、GBK、Unicode、UTF-32、UTF-16、UTF-8)
  5. 深入理解字符编码(ASCII、Unicode、UTF-8、UTF-16、UTF-32)
相关推荐
转转技术团队13 分钟前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding14 分钟前
这几个 Vibe Coding 经验,真的建议学!
前端·后端
gnip28 分钟前
SSE技术介绍
前端·javascript
掘金安东尼36 分钟前
蔚来 600 亿研发成本,信还是不信。。
面试·程序员·github
yinke小琪42 分钟前
JavaScript DOM节点操作(增删改)常用方法
前端·javascript
枣把儿1 小时前
Vercel 收购 NuxtLabs!Nuxt UI Pro 即将免费!
前端·vue.js·nuxt.js
望获linux1 小时前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件
爱编程的喵1 小时前
从XMLHttpRequest到Fetch:前端异步请求的演进之路
前端·javascript
喜欢吃豆1 小时前
深入企业内部的MCP知识(三):FastMCP工具转换(Tool Transformation)全解析:从适配到增强的工具进化指南
java·前端·人工智能·大模型·github·mcp
豆苗学前端1 小时前
手把手实现支持百万级数据量、高可用和可扩展性的穿梭框组件
前端·javascript·面试