12306网站的建设历程,wordpress好用插件推荐,军事新闻大事,鲜花网站建设论文百度文库本文为《人人都能读标准》—— ECMAScript篇的第12篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式#xff0c;并深入剖析了标准对JavaScript核心原理的描述。 ECMAScript有7种原始类型#xff0c;分别是Undefined、Null、Boolean、String、Number、BigInt、Symbo… 本文为《人人都能读标准》—— 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还允许使用指数e100e-2、.5e-3 ②十进制bigInt不允许有小数点也不允许使用指数e且必须在数字后面添加n如0n、100n ③非十进制整数包括 二进制整数在二进制数字0和1前面在0b或0B0b1010、0B1010 八进制整数在八进制数字07前面加0o或0O0o12、0O12 十六进制整数在十六进制数字09与Aa~Ff前面加0x或0X0x000A、0X000a ④非十进制bigInt与非十进制整数一样只是后面需要多加一个n0b1010n、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.33test .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)区间为[0xD8000xDBFF]以及一个低代理码元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进制表示是0xDE0AString.prototyoe.codePointAt(pos)返回位置pos上码点的值。 .codePointAt(0) // 128522
.codePointAt(1) // 56842String.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 除此以外其他的值都表示为NaNNot a Number。 精度丢失问题
使用浮点数表示数字的优势在于它能够表示的数字范围比整点数更广。但它的缺点在于有时候会有精度丢失的问题。稍有经验的前端都明白在JS中0.1 0.2 不精准等于 0.3核心原因在于双精度浮点数模型根本无法精确表示0.1、0.2、0.3。
你完全可以在上面我提供的转换器中测试一下把0.3转化成二进制再把对应的二进制反向转化一下得到的只是一个无限接近0.3的数字而不是0.3本身。