背景
QuickJS是Fabrice Bellard在2019年发布的一款支持ES6的JavaScript引擎。Fabrice Bellard是一位富有传奇经历的大佬。自己独立开发、发起和维护的耀眼明星项目有:FFmpeg、TCC、QEMU、JSLinux和LTEENB等等。FFmpeg是一个开源音视频处理库,提供了简单便捷统一的音视频处理接口,将广大的音视频开发程序员从音视频开发苦海中解放出来。QEMU是一个模拟器框架,绝大多数PC上的安卓模拟器都是基于它开发出来的。TCC是x86架构下,世界上最快的C语言编译器。JSLinux更是神,直接将linux系统运行到浏览器中。你以为这就完了?他一个人用10个月的时间,开发了一个可以在PC机上运行的4G LTE/5G NR基站程序。仅仅10个月啊,我连协议都看不完😓。毫不夸张的说,可能他一个人至少养活了全球上万个程序员。
QuickJS运行JavaScript原理
QuickJS在运行JavaScript源码时,先解析源码,源码解析成token,再根据生成字节码。字节码由操作码、操作数和标签组成。然后再对字节码进行优化。最后,再遍历字节码,根据操作码和操作数,执行操作和计算。
解析源码依赖的是Token。根据TC39制定的ECMAScript词法定义,将ECMAScript通用Token分成了以下几种
- IndentifierName
- PraviteIndentifier
- Punctuator
- NumbericLiteral
- StringLiteral
- Template
数字字面量
我们这篇文章的目的介绍大佬如何解析NumbericLiteral的。NumbericLiteral的意思是数字字面量。可能有同学学编程时,没有注意到字面量的定义,有点不太理解含义。字面量(literal)是用于表达一个固定值的表示法,又叫常量。举个例子:
let x = 10
let y = "hello,world"
console.log("nice~~~")
这个例子中,有两个变量分别是x和y。例子中还有三个字面量,其中有一个数字字面量、两个字符串字面量,分别是:10,“hello,world”, “nice~~~”。
- 这里的数字就是数学意义上的数字。
- 数字字面量区分:整数字面量、浮点数字面量(小数)、特殊值。
- 书写时直接书写字面量,不需要添加任何辅助符号。
科学计数法
var x = 123e5; // 12300000
var z = 123e-5; // 0.00123
JavaScript 数值始终是 64 位的浮点数,JavaScript 数值始终以双精度浮点数来存储,根据国际 IEEE 754 标准。
整数
整数字面量写法区分进制。可以被表示成十进制、八进制以及十六进制。在进行算术计算时或者参与程序,所有八进制和十六进制的数字都会被转换成十进制。
十进制
十进制最基本的数值字面量格式,基数为10。逢十进一,每个位数只能是0-9之间的数字。
八进制
八进制字面量必须带前导0、0O、0o。基数为8。逢八进一,每个位数只能是0-7之间的数字。(如果以0开头,每个位数上有超过0-7之间的数字出现,也就是8/9,强制忽视前面的0,直接将后面数字当做十进制)
下面是八进制转十进制的方法:
0O011 = 0 * 8^2 + 1 * 8^1 + 1 * 8^0 = 9
十六进制
十六进制字面量必须带前缀0x和0X。基数为16。逢十六进一,每个位数只能是数字0-9、字母a-f或A~F。
下面是十六进制转十进制的方法:
0x01000 = 0 * 16^0 + 0 * 16^1 + 0 * 16^2 +1 * 16^3 + 0 * 16^4 = 4096
浮点数
数学概念中的小数,浮点数不区分进制,所有的浮点数都是十进制下的数字。
注意:如果浮点数是大于0小于1的,可以省略小数点前面的0不写。
浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远远不如整数。
例如:0.1+0.2;结果不是0.3,而是:0.30000000000000004
Infinity 无穷
Infinity (或 -Infinity)是 JavaScript 在计算数时超出最大可能数范围时返回的值。
Infinity本身就是一个数字。
- 最小值:Number.MIN_VALUE,这个值为: 5e-324
- 最大值:Number.MAX_VALUE,这个值为: 1.7976931348623157e+308
- 无穷大:Infinity
- 无穷小:-Infinity
NaN 非数值
- NaN:not a number表示不是一个正常的数,但是还是一个 Number 类型的数字。这个数字 没办法用前面的表示方法表示。
- NaN与任何值都不相等,包括他本身。
- isNaN():判断一个数据是不是一个NaN。
补课
ASCII
为了让更多基础不强的同学能看懂,在此补下课。关于ASCII,引用下面的百度定义。我们存储类似英文字母、阿拉伯数字、英文标点符号等字符串到文本中,一般都遵循ASCII标准。比如,我们存储字符’A’到文本中,存储的不是字符‘A’,而是一个数字,这个数字就是ASCII表中,字符’A’对应的数字-- 65。
ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符
将数字字面量转化成数字
QuickJS将JavaScript代码先当作字符串读入内存。然后逐字节遍历这个字符串,当判断某一个字节的在字符 ‘0’ 和字符 ‘9’ 之间时,认为识别到了数字字面量的起始位置。然后将该字符串,转化成数字。
比如上面的例子中的第一行,程序从第一个字符 ‘l’ ('let’的第一个字符l)开始遍历,直到第9个字符,发现是字符 ‘1’ , 正好处于字符 ‘0’ 和字符 ‘9’ 之间。这时,程序认为这可能是一个数字字面量的开始。需要注意的是 ’ ’ – 空白也是一个字符,占用一个字节长度。然后继续遍历,直到下一个空字符。这很好理解,因为数字字面量是连续的,中间不能包含有空字符。
如此,我们便得到了一个完整的数字字面量 – 10。此时,它还是个字符串。如何将它解析成数字呢?看看大佬是如何做的。js_atof是将字符串转化成数字的函数,我们先放出来它的源码。
#define ATOD_INT_ONLY (1 << 0)
/* accept Oo and Ob prefixes in addition to 0x prefix if radix = 0 */
#define ATOD_ACCEPT_BIN_OCT (1 << 2)
/* accept O prefix as octal if radix == 0 and properly formed (Annex B) */
#define ATOD_ACCEPT_LEGACY_OCTAL (1 << 4)
/* accept _ between digits as a digit separator */
#define ATOD_ACCEPT_UNDERSCORES (1 << 5)
/* allow a suffix to override the type */
#define ATOD_ACCEPT_SUFFIX (1 << 6)
/* accept -0x1 */
#define ATOD_ACCEPT_PREFIX_AFTER_SIGN (1 << 10)
static JSValue js_atof(JSContext *ctx, const char *str, const char **pp,
int radix, int flags)
{
const char *p, *p_start;
int sep, is_neg;
BOOL is_float, has_legacy_octal;
int atod_type = flags & ATOD_TYPE_MASK;
char buf1[64], *buf;
int i, j, len;
BOOL buf_allocated = FALSE;
JSValue val;
/* optional separator between digits */
sep = (flags & ATOD_ACCEPT_UNDERSCORES) ? '_' : 256;
has_legacy_octal = FALSE;
p = str;
p_start = p;
is_neg = 0;
if (p[0] == '+') {
p++;
p_start++;
if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN))
goto no_radix_prefix;
} else if (p[0] == '-') {
p++;
p_start++;
is_neg = 1;
if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN))
goto no_radix_prefix;
}
if (p[0] == '0') {
if ((p[1] == 'x' || p[1] == 'X') &&
(radix == 0 || radix == 16)) {
p += 2;
radix = 16;
} else if ((p[1] == 'o' || p[1] == 'O') &&
radix == 0 && (flags & ATOD_ACCEPT_BIN_OCT)) {
p += 2;
radix = 8;
} else if ((p[1] == 'b' || p[1] == 'B') &&
radix == 0 && (flags & ATOD_ACCEPT_BIN_OCT)) {
p += 2;
radix = 2;
} else if ((p[1] >= '0' && p[1] <= '9') &&
radix == 0 && (flags & ATOD_ACCEPT_LEGACY_OCTAL)) {
int i;
has_legacy_octal = TRUE;
sep = 256;
for (i = 1; (p[i] >= '0' && p[i] <= '7'); i++)
continue;
if (p[i] == '8' || p[i] == '9')
goto no_prefix;
p += 1;
radix = 8;
} else {
goto no_prefix;
}
/* there must be a digit after the prefix */
if (to_digit((uint8_t)*p) >= radix)
goto fail;
no_prefix: ;
} else {
no_radix_prefix:
if (!(flags & ATOD_INT_ONLY) &&
(atod_type == ATOD_TYPE_FLOAT64 ||
atod_type == ATOD_TYPE_BIG_FLOAT) &&
strstart(p, "Infinity", &p)) {
{
double d = 1.0 / 0.0;
if (is_neg)
d = -d;
val = JS_NewFloat64(ctx, d);
}
goto done;
}
}
if (radix == 0)
radix = 10;
is_float = FALSE;
p_start = p;//计算有效数字开始的地方,有效数字指的是出去进制符号信息的数字,如:+0x988 中 988 就是有效数字
//跳过满足进制要求的字符
while (to_digit((uint8_t)*p) < radix
|| (*p == sep && (radix != 10 ||
p != p_start + 1 || p[-1] != '0') &&
to_digit((uint8_t)p[1]) < radix)) {
p++;
}
if (!(flags & ATOD_INT_ONLY)) {
// 判断是否是浮点数字中出现的小数点
if (*p == '.' && (p > p_start || to_digit((uint8_t)p[1]) < radix)) {
is_float = TRUE;
p++;
if (*p == sep)
goto fail;
//跳过满足进制要求的字符
while (to_digit((uint8_t)*p) < radix ||
(*p == sep && to_digit((uint8_t)p[1]) < radix))
p++;
}
//科学计数法
if (p > p_start &&
(((*p == 'e' || *p == 'E') && radix == 10) ||
((*p == 'p' || *p == 'P') && (radix == 2 || radix == 8 || radix == 16)))) {
const char *p1 = p + 1;
is_float = TRUE;
if (*p1 == '+') {
p1++;
} else if (*p1 == '-') {
p1++;
}
if (is_digit((uint8_t)*p1)) {
p = p1 + 1;
//遍历跳过所有数字
while (is_digit((uint8_t)*p) || (*p == sep && is_digit((uint8_t)p[1])))
p++;
}
}
}
if (p == p_start)
goto fail;
buf = buf1;
buf_allocated = FALSE;
len = p - p_start;//得到有效数字部分字符串长度
//看看buf够不够用,不够就重新申请
if (unlikely((len + 2) > sizeof(buf1))) {
buf = js_malloc_rt(ctx->rt, len + 2); /* no exception raised */
if (!buf)
goto mem_error;
buf_allocated = TRUE;
}
// 移除分隔符和进制前缀
/* remove the separators and the radix prefixes */
j = 0;
//加上负号
if (is_neg)
buf[j++] = '-';
//移除分隔符
for (i = 0; i < len; i++) {
if (p_start[i] != '_')
buf[j++] = p_start[i];
}
//截断字符串
buf[j] = '\0';
{
double d;
(void)has_legacy_octal;
//非十进制小数不符合规则
if (is_float && radix != 10)
goto fail;
//将字符串转化成double
d = js_strtod(buf, radix, is_float);
//封装成JSValue
val = JS_NewFloat64(ctx, d);
}
done:
if (buf_allocated)
js_free_rt(ctx->rt, buf);
if (pp)
*pp = p;
return val;
fail:
val = JS_NAN;
goto done;
mem_error:
val = JS_ThrowOutOfMemory(ctx);
goto done;
}
代码很简洁。这个函数的参数有五个,为了简化难度,我们只关注其中的两个即可,他们是:const char *str, int radix
。第一个参数是待解析的字符串,第二个参数是数字的进制。
进入程序之后,首先判断是否支持数字表达式中的下划线分割符,比如“9_9_9”。
接着判断字面量中是否存在符号。如果存在 ‘-’ ,则表明这个数字是负数。其中,如果设置了允许在符号之后接受前缀,就判断进制。如果不允许在符号之后接受进制标签,就跳过进制判断。
然后,判断数字的进制:二进制、八进制、十六进制或十进制(默认)。判断的方式是:先看看符号后一位(如果有)是不是 ‘0’ ,如果是0,就判断第二位。如果第二位是 ‘x’ 或 ‘X’,则表明是十六进制。如果第二位是 ‘b’ 或 ‘B’ ,则表明是二进制。八进制的判断稍微复杂一些,首先要区分是否是js是否运行在严苛模式,如果是,不允许以八进制表示字面量。如果不是,判断第二位是否为 ‘o’ 或 ‘O’,如果是,就说明是八进制。如果不是,再判断是否支持老式八进制表达式。先前版本的八进制表达时是以 ‘0’ 开头,后面是其它数字,没有进制符号。当然,十进制表示时,也有可能是以0开始的。那么如何区分两种进制呢?遍历这个数字字面量,判断这个字符串中是否包含字符 ‘8’ 和 ‘9’,如果是,那么这个数就是十进制,否则是八进制。因为八进制字面量中不可能存在 ‘8’ 和 ‘9’,两个字符。最后,需要再确认下hex进制标识符后面一个字符是否是符合进制的数字。比如:如果是十进制,只能在 ‘0’ 到 ‘9’ 中间;如果是十六进制,只能在 ‘0’ 到 ‘F’ 之间。
如果数字不是以 ‘0’ 开头,或者有符号标签同时不支持进制标签,就会判断数字是否是Infinity。如果是就,直接将 1除以0 得到正无穷大,如果前面判断是负的,就直接加上负号得到无穷小。最后,封装成JSValue,返回。如果不是Infinity,则继续下面的处理。
在进入转换数字之前,需要再次确认下字面量中,是否所有的数字都满足对应进制的要求。再移除掉字符串中的分隔符和进制标识符。
然后继续遍历字符串,直到字符不是数字为止。考虑到数字字面了可能存在小数点和科学计数法表达式。如果发现存在小数点,说明这个数字是个浮点型数字。之后,发现需要分别判断并继续遍历小数点或科学计数法后面的数字。
得到完整有效字面量字符串之后,调用 js_strtod() 函数来将字面量转换成double数字。这个函数的代码如下:
/* XXX: remove */
static double js_strtod(const char *p, int radix, BOOL is_float)
{
double d;
int c;
if (!is_float || radix != 10) {
uint64_t n_max, n;
int int_exp, is_neg;
is_neg = 0;
if (*p == '-') {
is_neg = 1;
p++;
}
/* skip leading zeros */
while (*p == '0')
p++;
n = 0;
if (radix == 10)
n_max = ((uint64_t)-1 - 9) / 10; /* most common case */
else
n_max = ((uint64_t)-1 - (radix - 1)) / radix;
/* XXX: could be more precise */
int_exp = 0;
while (*p != '\0') {
c = to_digit((uint8_t)*p);
if (c >= radix)
break;
if (n <= n_max) {
n = n * radix + c;
} else {
int_exp++;
}
p++;
}
d = n;
if (int_exp != 0) {
d *= pow(radix, int_exp);
}
if (is_neg)
d = -d;
} else {
d = strtod(p, NULL);
}
return d;
}
如果是十进制数整数,直接调用c标准库中的strtod直接将字符串转换成double类型数字。如果不是十进制数字整数或者是其它进制数字,会将其转换成十进制数字。
其它函数
to_digit
将字符串转成数字(按照ASCII表看就会明白)
static inline int to_digit(int c)
{
if (c >= '0' && c <= '9')
return c - '0';
else if (c >= 'A' && c <= 'Z')
return c - 'A' + 10;
else if (c >= 'a' && c <= 'z')
return c - 'a' + 10;
else
return 36;
}