【人人都能读标准】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…...

javaweb导师选择系统
本文以JSP为开发技术,实现了一个导师选择系统。导师选择系统分为三大模块,包括管理员:学员信息管理、导师信息管理、导师选择管理、导师分布图、公告信息管理、系统管理,学生:个人资料管理、导师选择管理、导师分布图管…...

LeetCode150 逆波兰表达式求值
题目: 给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。 注意: 有效的算符为 ‘’、‘-’、‘*’ 和 ‘/’ 。每个操作数(运算对象)都可以…...

【Node.js】项目开发实战(中)
开发用户的注册和登录接口步骤1,打开MySQL Workbench,打开自己的数据库进入创建用户信息表新建 ev_users表安装并配置mysql模块安装mysql模块新建db文件夹下index.js,导入并配置mysql模块安装bcryptjs对密码进行加密处理新建/router_handler/user.js中&a…...

记录一次 New Bing 英语陪练
记录一次 New Bing 英语陪练 Now I start to speak english to chat with you . Help me find the mistake in my word and help me improve my english I’m glad you want to practice your English with me. I can help you find the mistakes in your words and help you i…...

【Python】照片居然能变素描?不会画画但是咱会代码
文章目录前言一、准备二、下载预训练模型总结前言 Photo-Sketching 一个能将照片的轮廓识别出来并将其转化为“速写”型图像的开源模块。 比如,这只小狗: 经过模型的转化,会变成卡通版的小狗: 非常秀,这很人工智能…...

已解决正确配置git环境变量
已解决git没有配置环境变量,抛出异常ERROR: Cannot find command ‘git’- do you have ‘git’ installed and in your PATH?,附上正确配置git环境变量的教程 文章目录报错问题报错翻译报错原因解决方法《100天精通Python》专栏推荐白嫖80g Python全栈…...

【逐步剖C】-第十章-自定义类型之结构体、枚举、联合
一、结构体 前言:有关结构体的声明、定义、初始化以及结构体的传参等结构体的基本使用在文章【逐步剖C】-第六章-结构体初阶中已进行了详细的介绍,需要的朋友们可以看看。这里主要讲解的是有关结构体的内存问题。 1. 结构体的内存对齐 (1&…...

Windows Server 2016 中文版、英文版下载 (updated Mar 2023)
Windows Server 2016 Version 1607,2023 年 3 月更新 请访问原文链接:https://sysin.org/blog/windows-server-2016/,查看最新版。原创作品,转载请保留出处。 作者主页:sysin.org 本站将不定期发布官方原版风格月度更…...

Linux 4G 通信实验
目录4G 网络连接简介高新兴ME3630 4G 模块实验ME3630 4G 模块简介ME3630 4G 模块驱动修改ME3630 4G 模块ppp 联网测试前面我们学习了如何在Linux 中使用有线网络或者WIFI,但是使用有线网络或者WIFI 有 很多限制,因为要布线,即使是WIFI 你也得…...

华为OSPF技术详细介绍,保姆级,谁都能看懂(一)
目录 1、简介 2、OSPF基本原理 3、OSPF的特点 4、OSPF区域 5、路由器的类型 6、OSPF5种报文 7、后半部分内容 1、简介 OSPF(Open Shortest Path First,开放最短路径优先)是一个基于链路状态的内部网关协 议。目前针对IPv4协议使用的是OS…...