【人人都能读标准】12. 原始类型的编码形式
本文为《人人都能读标准》—— ECMAScript篇的第12篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。
ECMAScript有7种原始类型,分别是Undefined、Null、Boolean、String、Number、BigInt、Symbol。
本节中,我会先讲这7种原始类型的创建方式;然后我会谈到从标准的角度看,在原始类型上如对象一般调用方法是如何实现的;最后,我会对String和Number类型的底层编码形式进行深入的讲解。
原始类型的创建
创建原始类型的主要途径是字面量。我们从字面量表达式的产生式可以看到,ECMAScript有4种原始类型的字面量:
Null与Boolean
null字面量与Boolean字面量都非常简单,一个只能由终结符null
构成,另一个只能由终结符true
或false
构成。
Number与BigInt
数字字面量是我们在原理篇用来解释文法时,举的一个重要的例子。你还记得这张我们当时对数字字面量文法的“解构图”吗?
从解构的结果我们可以看到,数字字面量不仅可以生成Number类型,也可以生成BigInt类型,总的来说,它有5种大的合法结构:
①十进制数字:允许纯数字:
100.5
、1
;也允许以小数点开头:.0005
;还允许使用指数e:100e-2
、.5e-3
;②十进制bigInt:不允许有小数点,也不允许使用指数e,且必须在数字后面添加
n
,如0n
、100n
;③非十进制整数,包括:
二进制整数:在二进制数字(0和1)前面在
0b
或0B
:0b1010
、0B1010
;八进制整数:在八进制数字(0~7)前面加
0o
或0O
:0o12
、0O12
;十六进制整数:在十六进制数字(0~9与Aa~Ff)前面加
0x
或0X
:0x000A
、0X000a
;④非十进制bigInt:与非十进制整数一样,只是后面需要多加一个
n
:0b1010n
、0o12n
、0x000An
;⑤老式的8进制:在八进制数字前面加0来表示8进制:如
012
、00012
,现在这种写法已经不被推荐了。
此外,对于这些不同的数字字面量具体会产生什么样的数值,标准使用静态语义NumericValue来表示他们的“取值”过程。
String
我们同样可以像数字字面量一样对字符串字面量进行展开:
从展开的结果看,字符串字面量包括双引号字符串以及单引号字符串,且不包括字符串模版,字符串模版的文法在模版产生式中定义。
双引号字符串与单引号字符串,除了引号不同,每个字符的构成基本一样,都是由5条代换式组成,以双引号字符为例(图中框出部分):
-
①:表示字符可以由除了双引号
"
、行终结符、以及\
以外的所有Unicode字符构成,这是我们最常使用的字符; -
②:表示行终结符中的
<LS>
可以直接作为字符串字符使用; -
③:表示行终结符中的
<PS>
可以直接作为字符串字符使用; -
④:表示那些通过使用
\
转义后有特殊含义的字符或字符序列,包括:- 单转义字符,包括
\b
、\t
、\n
、\v
、\f
、\r
、\"
、\'
、\\
。 - 八进制转义序列:使用
\
、\0
开头。 - 十六进制转义序列:使用
\x
开头,且后面只能跟两个十六进制数字。 - 码点转义序列:使用
\u
开头,有两种写法,不带{}
的写法必须跟4个十六进制数字。
从对字符串取值的静态语义SV我们可以得知:八进制转义序列、十六进制转义序列、码点转义序列最终都会根据其数字的值转化为特定的码点,如下图所示(如果你对码点的概念不熟,后面的字符串编码形式部分能够帮到你):
基于这一点,我们可以使用不同的字符串字面量表示同一个换行符号
\n
(码点为10):"\n" === "\012"; // true - 八进制转义序列 "\n" === "\x0A"; // true - 十六进制转义 "\n" === "\u000A" // true - 码点转义序列(写法1) "\n" === "\u{A}" // true - 码点转义序列(写法2)
- 单转义字符,包括
-
⑤:第五条代换式是对行终结符的转义,这甚至使得你可以不借助字符串模版,直接在字符串中换行:
console.log("\我完全合法!")
undefined
undefined没有字面量文法,因而无法通过字面量创建。当我们在代码中如字面量一般地使用undefined
时,实际上,它访问的是全局对象上的undefined属性:
// undefined是全局对象上的属性
undefined in window // true
// 全局对象上的undefined属性不可修改
Object.getOwnPropertyDescriptor(window, "undefined") // {value: undefined, writable: false, enumerable: false, configurable: false}
undefined也不是保留字,所以你可以用undefined作为变量的标识符:
{let undefined = 1 // 声明一个名为undefined的变量let a // 未赋值的变量会初始化值为undefinedconsole.log(a === undefined) // falseconsole.log(a === void 0) // true
}
有的代码,你会发现像这里一样使用不太常见的void运算符,void运算符可以用来获得“纯正”的undefined,我们可以从它的求值语义中得到这个信息:
Symbol
Symbol也没有字面量,它只能通过内置对象Symbol创建:
Symbol("foo")
Symbol.for("foo")
标准中定义了一系列常用的Symbol,这些Symbol常常作为对象的插件使用。
在原始类型上“调用”方法
我们对于原始类型的方法调用并不陌生:
10.334524 .toFixed(2) // '10.33'
" test ".trim() // "test"
原始类型之所以可以调用方法,与成员表达式MemberExpression的求值语义息息相关。如果你经常读标准,你就会发现成员表达式是一个存在感非常高的表达式。
通过成员表达式的产生式,我们很容易发现,a.b
的形式会被解析为成员表达式。
而对a.b
形式的成员表达式的求值也非常简单明了:先获得.
左侧表达式的值,然后通过抽象操作 EvaluatePropertyAccessWithIdentifierKey获得这个值上对应的属性:
这里的“玄机”来自于第二步的抽象操作GetValue。不管第一步得到的是不是一个对象,GetValue会把第一步获得的结果使用抽象操作ToObject转化为对象。
于是,我们在ToObject中就能看到不同的数据类型,转换为对象的结果:
在这里,undefined与null无法进行转换,因为ECMAScript没有设计与这两个类型对应的内置对象,所以在undefined和null头上使用“成员表达式”会报错:
undefined[2]; // ❌:Uncaught TypeError: Cannot read properties of undefined (reading '2')
null[2] // ❌: Uncaught TypeError: Cannot read properties of null (reading '2')
其他的原始类型都会转化为各自的内置对象,于是,就可以使用各自的内置对象上的方法。
在转化结果的描述中,不同的对象都有一个叫做“内部插槽”的东西,使用[[]]
表示。关于内部插槽,我会在13.对象类型进行解释。
String类型的编码形式
String类型表示程序中的字符串。而谈到字符串,就离不开字符集(Character set) 与字符编码(Character Encoding) 。
现行世界中主要使用的字符集是Unicode,包含将近15万个字符。对于Unicode主要的两种字符编码形式分别是UTF-8以及UTF-16。其中HTML默认使用的是UTF-8,而ECMAScript默认使用的是UTF-16。
不管是使用UTF-8还是UTF-16,都可以表示Unicode中所有的字符。而这里有两个重要的概念:
- 码点(code point) :字符编码中,每个Unicode字符对应的数字映射。
- 码元(code unit) :字符编码中,码点的最小组成单位。
在UTF-8中,一个码元用一个字节(8位)表示,一个码点用1到4个码元表示;在UTF-16中,一个码元用两个字节(16位)表示,一个码点用1个或2个码元表示。
UTF-16的编码模型
在讲模型之前,有一个事情需要先搞清楚。在ECMAScript,数字类型的十六进制以0x
开头,如0x000A
;表示码点的字符串的十六进制以\x
或\u
开头,如"\x0A"
、"\u000A"
,如果你不弄清楚这一点,就常常会被它们写法的切换弄得晕头转向。
正如前面所说,在UTF-16中,一个码元使用两个字节表示,因此每个码元能够表示的区间是[0x0000, 0xFFFF]。在这个区间内,UTF-16又划分出一个高代理码元(high-surrogate code unit),区间为[0xD800,0xDBFF],以及一个低代理码元(low-surrogate code unit),区间为[0xDC00, 0xDFFF],如下图所示:
在ECMAScript中,码元是按照以下方式转化为码点的:
-
当一个码元即不是高代理码元,也不是低代理码元的时候,可以直接转化为码点;
-
当连续的两个码元c1、c2,前一个位于高代理码元,后一个位于低代理码元的时候,他们将构成一个代理对(surrogate pair),并通过以下的公式计算出码点的值:
(c1 - 0xD800) × 0x400 + (c2 - 0xDC00) + 0x10000
-
其他情况,码元都会被直接转化为码点。
比如,下面的代码将一个高代理码元与一个低代理码元组合,得到了新的码点:
console.log("\uD83D" + "\uDE0A')" // '😊'
UTF-16在实际代码中的应用
在ECMAScript中,字符串的length
方法计算的是字符串中码元的数量,而不是码点的数量:
"😊".length // 2
"😊"[0] // '\uD83D'
"😊"[1] // '\uDE0A'
在字符串方法的命名中,ECMAScript习惯使用charCode表示一个码元,使用codePoint表示一个码点,相关的方法包括:
-
String.prototype.charCodeAt(pos):返回位置pos上码元的值。
"😊".charCodeAt(0) // 55357, 16进制表示是0xD83D "😊".charCodeAt(1) // 56842, 16进制表示是0xDE0A
-
String.prototyoe.codePointAt(pos):返回位置pos上码点的值。
"😊".codePointAt(0) // 128522 "😊".codePointAt(1) // 56842
-
String.fromCharCode(codeUnit):把码元转化为字符。
String.fromCharCode(128522) // String.fromCharCode(55357, 56842) // 😊
-
String.fromCodePoint(codePoint):把码点转化为字符。
String.fromCodePoint(128522) // 😊 String.fromCodePoint(55357, 56842) // 😊
在这里,我们甚至还可以使用这些结果验证前面两个码元拼成一个码点的公式:
(c1 - 0xD800) × 0x400 + (c2 - 0xDC00) + 0x10000
因为我们已经知道,“😊”是由两个码元组成,数值分别为55357与56842,于是,我们可以在String.fromCodePoint
中应用这条公式:
String.fromCodePoint((55357 - 0xD800) * 0x400 + (56842 - 0xDC00) + 0x10000) // 😊
Number类型的编码形式
从上面我们可以看出,String类型中的每个字符实际上是由内存中16位或32位二进制表示的。而Number类型则是使用64位二进制表示,具体采用的是《IEEE 754-2019》定义的双精度浮点数格式,在其他编程语言中,这种浮点数类型也常用float64表示。
双精度浮点数模型
IEEE 754把64位分成3个部分:
- 符号位(sign):占用1位
- 指数位(exponent):占用11位
- 有效数位(fraction):占用52个位
懒得做图了,这里直接使用维基百科提供的图:
在这个模型中,任意数字都可以使用以下的公式表示:
在这条公式中,sign表示符号位,b表示有效数位,e表示指数位。
这里需要注意的是:这是一条混合了十进制和二进制数字的计算公式。 中间部分的(1.b51b52...b0)
是二进制表示,一个带有小数点的二进制。后面的2^e-1023
是十进制数字,不搞清楚这一点,你就无法得到正确的结果。
比如,数字1.5,可以用以下内存空间表示(这张图是我在一个float64二进制转换器中获得的,这里的Mantissa(尾数)相当于上面的fraction(有效数位)):
此时:
- 符号位为0,于是符号部分的计算相当于
-1^0
,表示正数; - 指数位为
01111111111
,转为十进制后是1023。于是,指数部分计算为2^(1023 - 1023)
,结果为1; - 在有效数位上,我们得到的是一个
1.1
的二进制小数,转化为十进制的方式以及结果是2^0 + 2^-1 = 1 + 0.5 = 1.5
所以,最终的计算结果为:
(-1^0) * (1.5) * 2^0 = 1.5
按理说,64位二进制应该可以表示2^64个数字。但实际上,ECMAScript的数字只有2^64 - 2^53 + 3
种,这是因为:当一个数字的指数位全是1的时候,被设计为不能通过以上的公式转化为实际的数字,这类特殊的数字有2^53
个(1个符号位 + 52个有效数位)。在ECMAScript中,这类特殊的数字会被转化为另外3种特殊的值:
-
如果此时有效数位全为0,符号位也是为0,在ECMAScript表示为+Infinity;
-
如果此时有效数位全为0,符号位为1,则表示为-Infinity;
-
除此以外,其他的值都表示为NaN(Not a Number)。
精度丢失问题
使用浮点数表示数字的优势在于,它能够表示的数字范围比整点数更广。但它的缺点在于,有时候会有精度丢失的问题。稍有经验的前端都明白,在JS中,0.1 + 0.2 不精准等于 0.3,核心原因在于:双精度浮点数模型根本无法精确表示0.1、0.2、0.3。
你完全可以在上面我提供的转换器中测试一下:把0.3转化成二进制,再把对应的二进制反向转化一下,得到的只是一个无限接近0.3的数字,而不是0.3本身。
相关文章:

【人人都能读标准】12. 原始类型的编码形式
本文为《人人都能读标准》—— ECMAScript篇的第12篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。 ECMAScript有7种原始类型,分别是Undefined、Null、Boolean、String、Number、BigInt、Symbo…...

VUE进行前后端交互
目录 一、 跨域 1. 什么是跨域? 2. 什么是本域? 3. 浏览器请求的三种报错 二、SpringBoot解决跨域问题其他前后端跨域请求解决方案 1. SpringBoot上直接添加CrossOrigin 2. 处理跨域请求的Configuration 3. 采用过滤器的方式 3.1 方式一 3.2 方式…...
ThingsBoard Gateway:物联网设备数据采集与集成的强大解决方案
文章目录ThingsBoard Gateway:物联网设备数据采集与集成的强大解决方案1\. ThingsBoard Gateway:概述2\. 主要特点与优势3\. 应用场景4\. 如何使用ThingsBoard Gateway:物联网设备数据采集与集成的强大解决方案 随着物联网(IoT&a…...
什么是镜像/raid
镜像(Mirroring)是一种文件存储形式,是冗余的一种类型,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像。可以把许多文件做成一个镜像文件,与GHOST等程序放在一个盘里用GHOST等软件打开后,又…...

【Python】如何有效比较两个时间序列在图形上的相似度?
文章目录前言一、1.准备二、实操1.使用Matplotlib可视化比较两个时间序列2.计算两个时间序列的相关系数:3.使用Python实现动态时间规整算法(DTW):总结前言 比较两个时间序列在图形上是否相似,可以通过以下方法&#x…...

JavaEE-常见的锁策略和synchronized的锁机制
目录常见的锁策略乐观锁和悲观锁轻量级锁和重量级锁自旋锁和挂起等待锁普通互斥锁和读写锁公平锁和非公平锁可重入锁和不可重入锁synchronized的锁机制synchronized特性锁升级/锁膨胀锁消除锁粗化常见的锁策略 乐观锁和悲观锁 乐观锁和悲观锁主要是看主要是锁竞争的激烈程度.…...

信息化,数字化,智能化是三种不同的概念吗?
前两年流行“信息化”,网上铺天盖地都是关于“信息化”的文章,这两年开始流行起“数字化”,于是铺天盖地都是“数字化”的文章。(这一点从数字化和信息化这两个关键词热度趋势就可以看出来)。 但点开那些文章仔细看看…...
【华为OD机试 2023最新 】 匿名信(C++ 100%)
题目描述 电视剧《分界线》里面有一个片段,男主为了向警察透露案件细节,且不暴露自己,于是将报刊上的字减下来,剪拼成匿名信。 现在又一名举报人,希望借鉴这种手段,使用英文报刊完成举报操作。 但为了增加文章的混淆度,只需满足每个单词中字母数量一致即可,不关注每个…...

硬件语言Verilog HDL牛客刷题day05 时序逻辑部分
1.VL29 信号发生器 1.题目: 题目描述: 请编写一个信号发生器模块,根据波形选择信号wave_choise发出相应的波形:wave_choice0时,发出方波信号;wave_choice1时,发出锯齿波信号;wave…...

Ajax 入门
前端技术:在浏览器中执行的程序都是前端技术。如 html、css、js 等 后端技术:在服务器中执行的长须,使用 Java 等语言开发的后端程序。servlet,jsp,jdbc,mysql,tomacat 等 全局刷新 使用表单…...

半导体器件基础06:发光二极管
说在开头:关于玻尔原子模型(1) 卢瑟福的模型面临着与经典电磁波理论的矛盾,按照经典电磁波理论,卢瑟福的原子不可能稳定存在超过1秒钟。玻尔面临着选择:要么放弃卢瑟福模型,要么放弃麦克斯韦伟…...

AutoCV第二课:Python基础
目录Python基础前言1.流程控制1.1 条件语句1.2 循环语句1.2.1 while循环语句1.2.2 for循环语句1.3 作业1.4 拓展-try except语法2.函数2.1 函数定义2.2 函数的参数2.2.1 位置参数2.2.2 命名参数2.2.3 默认参数2.2.4 可变参数2.2.5 参数展开2.3 递归函数2.3.1 递归函数定义2.3.2…...
LeetCode算法 打家劫舍 和 打家劫舍II C++
目录题目 打家劫舍参考答案题目 打家劫舍II参考答案题目 打家劫舍 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯…...

蓝桥杯刷题冲刺 | 倒计时10天
作者:指针不指南吗 专栏:蓝桥杯倒计时冲刺 🐾马上就要蓝桥杯了,最后的这几天尤为重要,不可懈怠哦🐾 文章目录1.有边数限制的最短路2.九进制转十进制1.有边数限制的最短路 题目 链接: 853. 有边数…...
个人练习-Leetcode-剑指 Offer II 109. 开密码锁
题目链接:https://leetcode.cn/problems/zlDJc7/ 题目大意:给出一个四位数字的密码锁,初始状态是0000,目标是targer。每一次转动只能让一个位的轮盘转动一下(0往后转是9)。有一个vector<string> dea…...
四个常见的Linux面试问题
四个常见的Linux面试问题。 刚毕业要找工作了,只要是你找工作就会有面试这个环节,那么在面试环节中,有哪些注意事项值得我的关注呢?特别是专业技术岗位,这样的岗位询问一般都是在职的工程师,如何在面试环节…...

15、接口(C#)
15.1 什么是接口 接口是指定一组函数成员而不实现它们的引用类型。所以只能类和结构实现接口。 15.2 声明接口 接口声明不能包含以下成员 数据成员静态成员 接口声明只能包含以下类型的费静态成员函数声明: 方法事件索引器属性 这些函数成员的声明不能包含任何实…...
C++中常见的容器类使用方法举例(vector、deque、map、set)
cpp中常见的容器类有vector、list、deque、map、set、unordered_map和unordered_set。 下面将举例直接说明各个容器的使用方法。 文章目录综合示例1. vector:动态数组,支持随机访问2. list:双向链表,支持双向遍历和插入删除3. de…...

什么是强缓存和协商缓存
什么是缓存 浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时候,如果是相同的URL&#…...
算法刷题之堆
1. heapq 堆 Python 中只有最小堆: import heapqa [] heapq.heappush(a, 3) # 添加元素 heapq.heappush(a, 2) heapq.heappush(a, 1) while len(a): # 判断堆的长度print(heapq.heappop(a)) # 弹出堆顶元素# 将列表转换为最小堆 nums [2, 3, 1, 4, 5, 6] hea…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...

基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...