js基石之数据类型一:类型分类&区别
js基石之数据类型二:类型判断
js基石之数据类型三:类型转换
js基石之Number:本质
js基石之Number:应用(数字运算,数字&字符串转换,不同进制表示&相互转换)
js基石之字符: ASCII,GBK,Unicode,utf-32,utf-16,utf-8,encodeuri,encodeuricomponent,base64
先有问题再有答案
:
字符是什么?
字符'a'在计算机中是如何存在的?
ASCII GBK Unicode 是什么 ?
utf-32 utf-16 utf-8 是做什么的 ?
'𠮷a'.length === 3 // true 为什么
如何可以使'𠮷a'的长度为2 ?
"👨👩👧".length === 1 或者 "👨👩👧".length === 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:万国码
它成功实现了每个数字代表唯一的至少在某种语言中使用的符号,目前,有一百多万个(从 0x000000
到 0x10FFFF
),这么一来即使全世界所有语言的字符都放进来也还绰绰有余,甚至放了很多 emoji 😂 表情字符。
既然可以表示这么多字符,那岂不是每个字符也需要更大的空间,不然岂不是会重复?确实如此,需要 4 个字节
来表示。但 Unicode 提供了多种字符编码方式,有些编码方式可以帮我们节省空间。
关于unicode我们需要知道的点有
:
- 收录了世界上的所有语言文字表示的字符,而且目前每年依然可以扩展
- 这么多的字符 最多使用32位可以完全覆盖 还有很多很多剩余空间....
- 依然兼容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变长的编码方式,使用一个字节作为基本单位编码
.
需要注意的是:
- 一个字节分配给ASCII 所以一个字节是 0xxxxxxx的格式
- 多个字节编码规则 开头有几个1表示有几个字节:
二个字节110xxxxx 10xxxxxx
三个字节1110xxxx 10xxxxxx 10xxxxxx
四个字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
对js的影响
获取码点
unicode字符集中每个字符对应的数字编号即为码点,在js中可以通过codePointAt 和 charCodeAt 方法获取对应的码点。
这个方法返回字符串中指定位置字符的 UTF-16 码位。如果该字符是基本多语言平面(BMP)的字符(即码位小于或等于 U+FFFF 的字符),那么这个方法返回的就是该字符的 Unicode 码位。但是,如果该字符是辅助平面的字符(即码位大于 U+FFFF 的字符),那么这个方法只会返回该字符的第一个 16 位单元(也就是代理对的高代理项)。
arduino
"好".charCodeAt() // 22909
这个方法返回字符串中指定位置字符的完整 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
- 特殊字符:URL 中有一些字符有特殊的含义,如问号(?)用于开始查询字符串,井号(#)用于开始片段标识符,斜杠(/)用于分隔路径的各个部分。如果你需要在 URL 的数据部分使用这些字符,你需要对它们进行编码,否则它们可能会被误解为它们的特殊含义。
- 非 ASCII 字符 :
URL 必须是 ASCII 字符集的一个子集
,这意味着所有的非 ASCII 字符 在 URL 中使用时都需要进行编码。 - 空格:URL 中不能直接包含空格,否则可能会导致错误。你需要使用 encodeURI 来将空格编码为 %20。
- 安全性:对 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 {世界} '