本系列包含ES2021规范全部28章内容,分两期来发,本期包含前21章,其余的部分在ES2021特性全集(二),建议按顺序阅读。
前言
ES2021是ECMAScript Language specification(也被称为js规范或ECMA-262)2021年的版本,也是第十二个版本(提供的链接是最新版本,如果官方有更新就不再是以上所说版本)。
其中一些特性不同浏览器实现情况不一,想知道浏览器实现情况,请参考caniuse;想要使用最新特性建议使用babel(想了解babel更多,可参考babel是怎么解决我们问题的)。
该规范是Ecma International Technical Committee 39,也就是TC39,编写的,是各种js实现的权威性规范,不管是在浏览器端还是在node.js,或者其他。所有的js引擎开发者都需要确保实现了该规范指定的特性。
我们通过阅读该规范可以实现以下几个目的
- 整体把握当前版本的ES标准,建构知识框架
- 对js运行细节有疑问时,可以用来参考,比如我在整理设计规范的享元模式时,想知道两个表达式进行比较时先计算哪边。
如果要读规范原文,初次接触者可能比较吃力,可先参考以下两个阅读指南:
本文的目的是通过规范学语言,因此侧重于讲和应用有关的部分,对于实现有关的会有所取舍,其中大章节会和原文保持一致。且之间关联性较弱可选择性阅读
正文开始👊
介绍
这一部分讲js规范的历史版本,更详细的介绍请参考JavaScript 之父 Brendan Eich 与 ES6 规范首席作者 Allen Wirfs-Brock 联合编写的JavaScript 20 年。
ES基于很多技术,最有名的包括JavaScript (Netscape) and JScript (Microsoft),这个语言是Brendan Eich在Netscape时发明的首次亮相是在Navigator 2.0 browser。
ES语言规范的开发开始于1996年11月,第一个版本在1997年6月的Ecma大会上获得通过,1998年6月的第2版和第1版的区别就是editorial。
第3版引入了正则表达式、增强了字符串处理和新的控制语句、try/catch错误捕获、更严谨的错误定义、数字输出的格式化等,在1999年12月发布。
在第3版发布以后,ES与万维网一起获得了广泛的采用,该万维网已经成为基本上所有Web浏览器都支持的编程语言。为开发第四版所做的大量工作。但是,该工作尚未完成,也没有作为es的第4版发布,但其中一些已合并到第6版的开发中。
第5版在2009年12月发布,包括访问器属性、反射、属性特征的编程控制、新的数组操作函数、JSON对象的支持以及更严格的错误检查和严格模式。2011年6月的5.1版包含一些小的改动。 👴
第6版准确来说是从第3版发布就开始了,到2015年用了15年才得以完成。其目标是为大型应用、库的创建和作为其他语言编译的目标语言做更好的支持。主要的提升包括模块、class声明、语块作用域、迭代器、用于异步编程的generators和promise、解构模式和尾调用优化。内置的库支持了另外的数据抽象包括maps、sets和二进制值数组以及在字符串和正则表达式中的unicode支持的补充字符。内置函数也可以通过子类扩展。
从2016年开始,ES开始了一年一次的迭代节奏,ES2016修改了之前版本的Bug和做了其他提升,并为此开发了几个辅助的软件工具包括Ecmarkup, Ecmarkdown, and Grammarkdow。此外还添加了几个乘方操作符并添加到Array.prototype一个方法:includes。
ES2017引入了async函数、共享内存、Atomics以及更小的语言和库的增强。async函数通过提供返回promise的语法提升了异步编程的体验。共享内存和Atomics引入了memory model允许多agent的程序通过原子操作通信来确保即使在并行的cpu也能保证执行顺序。也在Object上添加了新的静态方法:Object.values, Object.entries, and Object.getOwnPropertyDescriptors。
ES2018通过异步迭代协议和异步generator引入了对异步迭代的支持,也引入了4个正则表达式的新特性:dotAll标记、具名捕获组、unicode属性转义和向后断言。最后还有对象rest和spread属性。
ES2019引入了新的内置函数:在Array.prototype上添加了flatandflatMap来打平数组,Object.fromEntries用来直接将Object.entries返回的值还原成新的对象,将内置的广泛实现但非标准的String.prototype.trimLeft and trimRight取一个好的名字trimStart and trimEnd。另外还有一些语法更新包括catch参数可选、允许在字符串中使用 U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)来对齐JSON。要求数组sort排序稳定、要求JSON.stringify不管输入如何都要返回格式正确的UTF-8、通过明确要求返回相应的原始源文本或标准占位符对Function.prototype.toString 。
ES2020为字符串引入了matchAll方法来为全局正则表达式生成的所有匹配对象生成迭代器;import(),一个异步引入模块的语法;BigInt,一个任意精度的整数原始类型;Promise.allSettled,一个新的不走捷径的promise组合;globalThis一个访问this值的通用方式;专门用在模块中的export * as ns from 'module';增加了for-in枚举顺序的标准化; import.meta,一个在模块中使用包含模块上下文信息;并为null和undefined添加了两个语法特性:nullish coalescing(即??,用于值选择)和optional chaining(即?.,用于对可能的null或undefined进行短路操作)。
1. 范围
该标准定义了ES 2021通用编程语言。
2. 一致性
一个符合标准的ES实现要遵守的规则,比如一定要支持所有的类型、值、对象、属性、函数和编程语法语义、 一定要以最新标准的Unicode Standard and ISO/IEC 10646解释文本输入。
其他不提。
3. 参考规范
为了实现本规范还需要参考的规范
4. 概述
ES最初设计为一个脚本语言,现在作为一门通用的编程语言被广泛使用。对应运行时环境(runtime environment,比如node.js)除了要实现规范中定义的对象和能力,还要有特定环境的特性,比如输入输出、计时器等。
ES是基于对象的:基本语言和宿主能力都是由对象提供的,一个ES程序是一组可通信的对象。在ES中一个对象是0个或多个属性(properties)的集合,每个属性都有特性(attributes)决定每个property怎么使用,比如当一个属性的Writable特性设为false,就不能重新赋值。属性是可以包含其他对象、原始值或函数的容器。
ES包含以下七种内置类型的原始值Undefined, Null, Boolean, Number, BigInt, String, and Symbol。一个对象是内置类型Object的一个成员,函数是可调用的对象。
ES定义了一组内置对象,包括global object、基本的语言运行时语义对象(Object, Function, Boolean, Symbol和不同的 Error对象)、用来表示和操作数值的对象(Math, Number, and Date)、文本处理对象(String and RegExp)、可索引对象(Array和不同的Typed Arrays)、Keyed集合(Map and Set)、支持结构化数据的对象(JSON object, ArrayBuffer, SharedArrayBuffer, and DataView)、控制抽象化对象(generator函数和 Promise)和反射独享(Proxy and Reflect)。
也定义了一系列内置操作符,包括各种一元运算,乘法运算符,加法运算符,位移运算符,关系运算符,等式运算符,二进制运算符,二进制逻辑运算符,赋值运算符和逗号运算符
模块可以将程序分成多个语句和声明,每个模块明确地标识它所使用的其他模块提供的声明,哪些声明可供其他模块使用。
ES语法有意和java语法类似,但为了使其成为一种易用的脚本语言又是比较松散的,比如变量不需要类型声明(有类型声明的js叫typescript,想了解ts更多可参考按照新的思路再学一遍typescript)。
Object
尽管ES包含class的语法,但是并不是像C++那样以class为基础。对象可以通过很多方式创建,比如字面量或构造函数。每个构造函数都是一个带有prototype属性的函数,这个属性用来实现基于prototype的继承和属性共享。要通过new来使用构造函数,不使用new的结果取决于构造函数本身。
每个通过构造函数创建的对象都有一个到构造函数prototype属性的隐式引用(被称为对象的原型,用__proto__表示),一个prototype也可能有一个非null的__proto__ ,以此类推,这就被称为原型链(可参考JavaScript Prototype and Prototype Chain explained或从探究Function.proto===Function.prototype过程中的一些收获 )。
比如
var Foo=function(){}
var foo=new Foo()
foo.__proto__===Foo.prototype//true
foo.constructor===Foo //true
在js的世界里有两个特殊的对象,Object和Funciton,Obejct.prototype是所有常规对象(不包括直接通过Object.create(null)创建的和通过setPrototypeOf等显式修改过的部分对象)原型链的尽头(Obejct.prototype.__proto__等于null),包括Function.prototype,即
Function.prototype.__proto__===Object.prototype //true
new Object().__proto__===Object.prototype //true
其中可调用对象(即函数,包含[[call]]内部属性)的原型链到达Obejct.prototype之前需要先经过Function.prototype,包括Object、其他内置可实例化对象(比如Array),和直接实例化的Function(比如Foo),甚至Function本身
Foo.__proto__===Function.prototype //true
Object.__proto__===Function.prototype//true
Array.__proto__===Function.prototype//true
Function.__proto__===Function.prototype//true
就这样,在这些离js实现最近的对象基础上通过继承实现各种子类,就可以不断延长原型链,这就是原型继承本来的样子。
严格模式
严格模式为js提供了一种严格变体的选项,其语法和运行时行为和普通代码不同,一个不支持严格模式的浏览器和一个支持的浏览器运行严格模式的代码的表现是不一样的。
严格模式相对于普通的js有以下变化
- 将可以接受的错误转化成error,比如试图删除不能删除的属性
- 修复了让js引擎难以优化的问题,比如eval的使用
- 仅用了可能会在将来版本定义的语法,比如增加了保留关键字。
具体的变化请参考Strict mode
开启严格模式
严格模式可以应用到整个脚本或单独的函数,但不能应用于块作用域。
- 为脚本开启严格模式
在其他语句之前添加"use strict"; (or 'use strict';)
// Whole-script strict mode syntax
'use strict';
var v = "Hi! I'm a strict mode script!";
- 为函数开启严格模式
要将"use strict";完整的放在函数体语句之前,比如
function strict() {
// 函数级别严格模式语法
'use strict';
function nested() {
return "And so am I!";
}
return "Hi! I'm a strict mode function! " + nested();
}
function notStrict() {
return "I'm not strict.";
}
- 为模块设置严格模式
模块的全部内容默认是严格模式
本规范的结构
第5章定义了本规范的符号约定
第6章到第9章定义了ES程序运行的执行环境(execution environment)
第10章到第16章定义了语言特性的语法编码和执行语义
第17到27章定义了ES标准库,包含ES程序运行时可用的所有标准对象
第28章描述了支持SharedArrayBuffer的内存访问的内存一致性模型和原子对象的方法
5. 符号约定
这里将用于本规范的一些约定,只介绍后文会涉及到的概念
- Parse Node:源码会被解析成一棵树,树上的每个节点就是一个解析节点(Parse Node)。
6. ES数据类型和值
本规范我们操作的每个值都有一个类型,可能的类型都在这一章定义。类型又被细分为ES语言类型和规范类型,其中语言类型对应于ES程序中直接操作的值,规范类型值用于在实现ES语言过程中使用,用来描述各种原始或中间结果,不要暴露给语言的使用者。
语言类型
包括七种原始值的类型和Object
Undefined
只有一个值,即undefined,任何没有赋值的变量都有一个undefined
Null
也是只有一个值null,表示变量未指向任何对象,可以把null当做尚未创建的对象。
Boolean
代表一个逻辑实体,包含两个值false和true
String
是一串有序的0个或者多个(最多2^53-1个,数组最多长度2^32-1)16位unsigned integer序列,通常用来表示程序中的文本数据,序列中的每个元素都被当成一个utf-16并占了一个位置,这些位置用非负整数索引,第一个元素的索引是0,空序列没有元素。
Symbol
每个Symbol类型的值都是唯一且不可变的,可以做对象的属性名,
Number
ES中的数字类型是双精度IEEE754 64位浮点类型,
- 数符(S,sign)1位,表示正负,0为正1为负
- 阶码(E,exponent)11位,表示小数点偏移的位数,实际使用的值=阶码-偏置值。
- 尾数(M,Mantissa)52位,表示数字的小数部分
当E不全为0或者1为普通值,尾数最高位有个默认的1,可表示为
精度
精度受尾数和数符影响,其中尾数52位,加上最高位默认的1一共53位,即整数的准确表示范围为±2^53-1,在js中称为安全整数(Number.MIN_SAFE_INTEGER,Number.MAX_SAFE_INTEGER),最大精度是安全整数(9007199254740991)的位数即16位。
数字范围
阶码范围为 [−1022, 1023],绝对值最大时,尾数全都是1(约等于2),得2^1023*2,即2^1024,在js中用Number.MAX_VALUE表示,超过这个数用Infinity表示
绝对值最小时,尾数全是0(等于1),得2^(-1022)。
但是如果取下面的非规格化数,尾数最高位默认为0,即小数点向后移动52位。得2^(-1022-52)=2^(-1074),在js中用Number.MIN_VALUE表示,小于这个值记为0。
即最终可表示的数字范围为[-2^1024,-2^(-1074)]∪[2^(-1074),2^1024]。
- 当E全为0,M不全为0,尾数最高位默认0,表示非规格化数
- 当E全为0,M全为0,表示±0
- 当E全为1,尾数全为0时表示无穷大±∞
- 当E全为1,尾数不全为0时,表示NaN(not a number)
BigInt
表示一个没有精度限制的整数
Object
一个对象是一个属性的集合,每个属性要么是一个数据属性要么是一个访问器属性。
- 数据属性将key关联一个ES语言类型的值,以及一系列布尔特性
- 访问器属性将key关联一个或两个访问器函数,以及一系列布尔特性。访问器函数用于保存或者取出和这个属性关联的ES语言值
属性使用key值标记,一个属性的key要么是string要么是symbol。 属性key用来访问属性和其他值,对于属性有两种访问方式:get和set,分别为获取和赋值。
属性的特性
在本规范中特性用于定义和解释对象属性的状态,一个数据属性相关的key有以下特性
- [[value]] 任何ES语言类型值,是通过get获取的值
- [[writable]] 布尔值,是否可以修改
- [[Enumerable]] 布尔值,是否可以被
for-in枚举 - [[Configurable]] 布尔值,是否可以删除、修改为访问器属性、修改特性(除了修改value和将writable变为false)
访问器属性相关的key有以下特性
- [[Get]] Obejct或者undefined, 如果是个对象就一定是function对象,每次访问这个属性时该函数会调用
- [[Set]] Object或undefined,如果是个对象就一定是function对象,赋值时调用
- [[Enumerable]] 布尔值,是否可以被
for-in枚举 - [[Configurable]] 布尔值,是否可以删除、修改为数据属性或者修改特性
对象内部方法(Internal Methods)和内部插槽(Internal Slots)
所有对象在逻辑上都是属性的集合,但是有多种形式的对象它们访问和操作属性的方式不同。
在ES中,对象实际的含义通过调用特定的内部方法实现,在ES引擎,每个对象都关联了一些内部方法来定义他们的运行时行为,这些内部方法不是ES语言的一部分,在这里只是为了说明。但是在实现内部一定要有内部方法对应的表现。
内部方法名是多态的,这意味着不同对象调用内部方法的表现可能不一样。
内部的插槽对应于对象相关的内部状态,用于不同的ES规范算法。内部插槽并不是对象属性不会被继承。取决于特定的内部插槽,这些状态可能包含任意ES语言类型的值或者特定ES规范类型的值,默认为undefined。ES语言不会指定直接方式访问这些内部插槽。
内部方法和内部插槽在规范中都用双方括号[[ ]]包围.
对象分为两类,一类是普通对象( ordinary object),他们符合以下标准
- 对于以下内部方法
- [[GetPrototypeOf]]
( ) → Object | Null获得继承的对象,即__proto__ - [[SetPrototypeOf]]
(Object | Null) → Boolean设置被继承对象 - [[IsExtensible]]
( ) → Boolean决定是否允许被添加属性 - [[PreventExtensions]]
( ) → Boolean设置为不可添加属性 - [[GetOwnProperty]]
(propertyKey) → Undefined | Property Descriptor返回自身的属性访问器 - [[DefineOwnProperty]]
(propertyKey, PropertyDescriptor) → Boolean创建或修改属性,其中propertyKey是属性key - [[HasProperty]]
(propertyKey) → Boolean判断是否有自身或继承指定属性 - [[Get]]
(propertyKey, Receiver) → any返回指定属性 - [[Set]]
(propertyKey, value, Receiver) → Boolean设置属性 - [[Delete]]
(propertyKey) → Boolean删除属性 - [[OwnPropertyKeys]]
( ) → List of propertyKey返回对象所有属性的LIST
- [[GetPrototypeOf]]
- 如果有[[Call]] 内部方法
- [[Call]]
(any, a List of any) → anyfunction对象就是有[[Call]] 内部方法的对象,会通过调用时执行
- [[Call]]
- 如果有[[Construct]] 内部方法
- [[Construct]]
(a List of any, Object) → Object一个支持 [[Construct]]的一定支持[[Call]],即一个构造函数一定是个函数当new或者super调用时执行
- [[Construct]]
如果不符合就是奇异对象(exotic object),规范中通过各自的内部方法识别不同其一对象,如果只是行为相似不好使。
已知内部对象
已知内在对象是本规范中算法显式引用的内置对象,通常有特定realm的标志。
一个内在对象用%name%表示,具体见Table 8: Well-Known Intrinsic Objects。
ES规范类型
规范类型用于在算法中用于描述ES语言构造和ES语言类型的元值,下面介绍几种会在本文涉及到的类型。
List
list 类型在new表达式、函数调用和其他算法中需要简单有序列表中被用来描述参数列表的值
Record
用来在算法中描述数据集合,一个Record类型的值包含一个或多个具名字段,每个字段的值要么是一个ES值,要么是一个record。字段name通过被双方括号包起来,比如属性描述符{ [[Value]]: 42, [[Writable]]: false, [[Configurable]]: true }
Completion Record
是一个Record,用来描述值和控制流(比如break, continue, return and throw)的runtime传播。
Completion类型的值是Record,它们的字段如下定义
- [[Type]] normal, break, continue, return, or throw之一,表示发生的完成类型
- [[Value]] 任何ES语言值或空,表示生成的值
- [[Target]] 任何字符串或者空,用来直接控制传播的label
术语abrupt completion表示非normal type的Completion
Environment Record
用来描述嵌套函数或者块中的名称解析
Abstract Closure
一个抽象闭包(Abstract Closure)和一个值的集合一起用来指一个算法步骤。抽象闭包是元值使用函数风格调用,比如 closure(arg1, arg2),调用时会执行对应的算法步骤。
在一个创建抽象闭包的算法步骤中,值会被一个别名列表被捕获。当一个抽象闭包创建后,它会捕获每个别名相关的值。当闭包被调用时,每个捕获的值都由用来捕获值的别名引用。
如果一个抽象闭包返回一个 Completion Record,其[[type]]只能是normal或throw
抽象闭包作为其他语法的一部分被创建,比如
- 让加数(
addend)为41 - 让闭包(
closure)有个参数x,其捕获加数,并且在调用时执行以下步骤:返回x+addend - 让
val为closure(1) - 断言:val是42
Set
在内存模型中用来表示无序元素的集合,其中的元素不会重复,
7. 抽象操作
用在规范内部帮助规范语义定义,包括类型转换、测试和比较操作符、对象操作和迭代对象操作。
8. 可执行代码和执行上下文
原规范这一章分为两部分,其中弱引用(WeakRef)部分会在内存管理部分展开,本章主要讲其余的,即js执行过程中的实现细节,相关概念如下
环境记录
环境记录(Environment Record)是一个规范类型,基于词法嵌套结构将一个标识符和特定变量与函数结合起来。通常一个环境记录都和一个语法结构(比如函数声明或块语句)有关,每次对应代码执行,一个新的环境记录就被创建来记录对应的标识符绑定,这时候就可以通过这些标识符取对应的变量。
每个环境记录都有一个[[OuterEnv]]字段,是另一个环境记录或者null,用来表示环境记录间的逻辑嵌套关系。
环境记录类型可以认为是一个面向对象层次关系,一个抽象类,三个具体类(其中全局环境记录位于作用域的最外层,用来绑定全局对象的属性,和在script顶层的声明,一个全局环境在逻辑上是单独的记录,但被指定为一个对象环境记录和声明环境记录的组合),还有另外的子类:
- Environment Record (abstract)
- declarative Environment Record 对应变量声明、函数声明或catch语句等语法元素
- function Environment Record
- module Environment Record
- object Environment Record 对应with语句等语法元素
- global Environment Record
- declarative Environment Record 对应变量声明、函数声明或catch语句等语法元素
执行上下文
执行上下文被ES的实现用来跟踪运行时的计算。
任何时候每个agent最多有一个实际执行代码的上下文,这被称为正在执行的上下文(running execution context)。
执行上下文栈( execution context stack)用来跟踪执行上下文,正在执行的上下文总是在栈顶,当控制传递给和执行上下文没关的可执行代码时就会创建新的执行上下文,新创建的执行上下文就会压栈称为正在执行的上下文。
一个执行上下文需要指定状态来跟踪相关代码的执行进度,每个执行上下文包含以下state component
- Code evaluation state 包含用于执行、暂停、恢复代码执行的任何状态
- Function 如果执行上下文的代码是在函数中,这个值就是那个函数对象,否则就是null
- Realm 相关的Realm Record,我们会在下面单独讲。
- ScriptOrModule 相关的 Module Record or Script Record,如果是在InitializeHostDefinedRealm创建的上下文则是null
- Generator Generator执行上下文相关联的generator object
- VariableEnvironment 另外一些环境记录,由
var声明创建的一些绑定 - LexicalEnvironment 在这个执行上下文中用来解析标识符引用的环境记录,包括使用
let和const等生成的。
创建执行上下文
执行上下文只是一种机制,不会被代码直接访问到,我们不能直接创建,但是就像前面提的,当一段和执行上下文还没关联的代码被执行时就会创建新的执行上下文,如
function recursive(n = 0) {
if (n > 1) throw new Error();
recursive(++n);
}
recursive();
console.log("Will never be logged...");
从一个全局上下文开始,然后由递归函数创建新的执行上下文
直到抛出错误,此时就能看到实时嵌套着的各种上下文,最外层是全局上下文
如果不抛错
function recursive(n = 0) {
if (n > 1) return;
recursive(++n);
// *
}
recursive();
console.log("Will be logged!");
此时的上下文栈会这样
各个执行上下文以后进先出的顺序入栈出栈,最上面的就是正在执行的上下文。
realms
在计算之前,所有代码都需要关联一个realm.从概念上说,一个realm包含一系列内置对象、一个全局环境、全局环境作用域加载的所有代码以及其他相关的状态和资源。
一个realm在规范中用 Realm Record表示,主要用于提供内在值和访问全局对象。一个 Realm Record有以下字段
- [[Intrinsics]] 用于realm相关代码的内在值
- [[GlobalObject]] 这个realm的全局对象
- [[GlobalEnv]] 这个realm的全局环境记录
- [[TemplateMap]]
- [[HostDefined]] 宿主定义的其他值
InitializeHostDefinedRealm
realm是执行上下文的一个state component,当创建第一个执行上下文时需要有一个realm进行关联,即在所有代码执行前需要执行InitializeHostDefinedRealm抽象方法对realm进行初始化。包括以下步骤
- 通过
CreateRealm()创建realm - 让新的执行上下文称为
newContext - 设置
newContext的Function为null - 设置
newContext的realm为realm - 设置
newContext的ScriptOrModule为null - 将
newContext压入执行上下文栈,现在newContext是正在执行的上下文 - 如果宿主需要一个exotic对象作为realm的全局对象,让
global是一个宿主定义的对象,否则让global是undefined,这意味着ordinary对象将被创建成为全局对象。 - 如果宿主需要
this绑定realm的全局作用域返回的对象,而不是全局对象,则将thisValue是一个宿主定义的对象,否则thisValue是undefined,这意味着realm的this将绑定全局对象 - 执行
SetRealmGlobalObject(realm, global, thisValue). - 让
globalobj为 SetDefaultGlobalBindings(realm) - 在
globalobj创建任何全局对象的属性 - 返回
NormalCompletion(empty)
agent
一个agent由一系列执行上下文、一个执行上下文栈、一个正在执行的上下文、一个Agent Record和一个执行线程组成。除了执行线程,其他都是这个agent独有的。
一个agent的执行线程在它的执行上下文独立于其他agent地执行job,除非执行线程是跨agent的,前提是共享线程的agent的[[CanBlock]]属性为true,比如一些浏览器会跨互不相干的tab共享执行线程。 当一个agent的执行线程执行jobs,对应代码的surrounding agent就是这个agent。这些代码可以访问对应agent的正在执行的上下文、上下文栈和Agent Record的字段。
一个agent要么是web页面的主程序要么是一个web worker
agent cluster
一个agent集群是一个可以通过内存共享(比如SharedArrayBuffer)通信的agent,每个代理都属于一个集群。
比如Worker中用来通信的postMessage()就是基于SharedArrayBuffer。
job
前面提到一个执行线程要执行job,那么job是什么呢,规范中关于job的定义不同版本介绍区别挺大的,这里按当前版本来解释。
一个job是一个没有参数的抽象闭包,会在程序中当前没有其他计算时初始化一个计算。
job是通过宿主环境调度执行的,这个规范描述了宿主钩子HostEnqueuePromiseJob来调度一种job。宿主也可以定义另外的抽象操作来调度job,这些操作需要接受抽象闭包为参数并使其在将来某个时间执行。这些实现要遵守以下需要
- 在将来某个时刻,当没有正在执行的上下文且执行上下文栈为空,实现必须:
- 执行任何的宿主定义的预备步骤
- 调用job
- 执行任何的宿主定义的清理步骤
- 任何时候都只能有一个活跃的job
- 一旦一个job开始执行,只有完成才能允许其他job开始
- 这个抽象闭包一定要返回一个normal completion,实现自己的错误处理。
宿主不需要统一对待调度的job,web浏览器和node.js都把处理promise的job设为高优先级。
看到这里我们直到job其实就是一些将来执行的算法步骤,其中promise优先级高一些,再加上一些另外的限制。每个job具体怎么执行的,我们需要参考whatwg html 规范中的event-loop
event loop
ES规范规定了V8等实现注意的问题,但是实际使用ES的是浏览器等宿主,因此我们以html为例看一下相关规定。
为了协调事件、ui、scripts、渲染、网络请求等工作,用户代理必须使用event loop。
每个event loop有一个或多个task queues,注意task queue并不是queue,而是sets,因为event loop第一步是选择第一个可执行的task,而不是通过出队执行第一个任务。
event loop封装的算法对应于这样的工作
- events,通过专门的任务在特定eventTarget对象上分发事件(不是所有事件都这么分发)
- parsing,html解析器通过任务进行解析
- callback,通过专门的任务调用回调
- 使用资源,比如异步fetch资源
- 响应dom操作
每一个event loop有一次微任务队列(microtask queue),是一个微任务组成的队列,最初是空的。一个微任务是一种通俗的称呼,指的是通过微任务队列算法创建的任务。
每个event loop有一个执行微任务检查的布尔值,初始值是false,用来避免重复执行。
换句话说,宏任务就是指的一个个浏览器为了协调工作组织的一个消息队列,微任务就是指的前面说的job(在ES规范中只介绍了一种promise相关的job,在html规范中又加入了queueMicrotask),job组成一个job queue,在浏览器中job的优先级高,因此会比任务队列中的任务优先执行。
微任务是由任务创建的,当一个任务完成把控制权交给用户代理进行下一次loop前,如果存在微任务队列就依次执行队列中的所有微任务,注意微任务本身也会生成微任务,直到微任务队列为空,然后执行必要的渲染,再进行下一个任务。
这里推荐一个可视化执行js的网站,效果见下图
HostEnqueuePromiseJob ( job, realm )
是一个宿主定义的抽象操作,用来调度抽象闭包job在未来某个时刻执行。算法用的抽象闭包被用来处理promise或者其他来和promise相同的优先级进行操作。
如果realm是null,没有ES代码会被执行,也没ES对象会被创建。WHATWG HTML specification使用realm的检查来作为能不能运行script的方法。
参考链接
除了以上提供的对应链接外,本部分还参考了以下
- The ECMAScript “Executable Code and Execution Contexts” chapter explained
- Using microtasks in JavaScript with queueMicrotask()
- EventLoop
- JavaScript job queue and microtasks
- Macrotasks and Microtasks
9. 普通和奇异对象的表现
普通对象的内在方法和内在插槽
所有普通对象都有一个内部插槽,叫[[Prototype]],这个插槽的值要么是null要么是一个用于实现继承的对象。
每个普通对象都有一个布尔值的内部插槽[[Extensible]],用来实现扩展相关的内部方法。
函数对象
函数对象(function objects)在一个词法环境中封装了参数化的代码,并且支持代码的动态计算。
函数同时也是一个普通函数,除了上面提的两个插槽还包括
- [[Environment]] 函数所在的环境记录,当计算函数的代码时作为outer environment
- [[FormalParameters]] 是定义在参数列表中的根解析节点(Parse Node)
- [[ECMAScriptCode]] 定义在函数体的根解析节点
- [[ConstructorKind]] 用base或derived表示是不是一个派生的class
- [[Realm]] 函数所在的Realm Record
- [[ScriptOrModule]] 函数创建所在的Script Record or Module Record
- [[ThisMode]] 定义了this引用的解释方式,可取值lexical | strict | global
- [[Strict]] 一个布尔值,代表是不是一个严格模式的函数
- [[HomeObject]] 如果函数使用了super,则该值表示其__proto__对象
- [[SourceText]] 定义函数的源文本
- [[IsClassConstructor]] 布尔值,代表是不是class 构造函数
内置函数对象
即使是exotic objects也要有[[Prototype]], [[Extensible]], and [[Realm]] 三个内置插槽。
内置奇异对象
规范定义了很多内置的奇异对象,奇异对象和普通对象表现类似,但也有一些不同之处,如下
- 绑定函数的奇异对象
- 数组奇异对象
- 字符串奇异对象
- Arguments奇异对象
- Integer-Indexed奇异对象
- 模块命名空间奇异对象
- 不可变的Prototype奇异对象
Proxy对象
代理对象(注意和agent区分)是一个奇异对象,有一个内部插槽[[ProxyHandler]],它的值是一个对象,被称作代理的handler对象或null。具体的用法见后面的Proxy部分
10. 源码
源文本
ES代码用unicode表示,ES源文本是一系列码点。从U+0000 to U+10FFFF的所有码点都被允许使用
源码的类型
有四种类型的源码
- 全局代码,即ES script
- eval代码
- function代码
- module代码
11. 词法
这一部分讲js的语法,比如控制符或保留字,具体可参考mdn相关内容
以下部分不以规范为主,基本主要参考JavaScript reference
12. 表达式
这里会讲各种表达式、操作符和关键字。
this
一个执行上下文(execution context )的属性,在非严格模式表示一个对象的引用,在严格模式可以是任意值。
全局上下文
严格模式没变化,指向一个全局变量globalThis
函数上下文
this取决于函数怎么调用,如果上下文中this没有设置(直接调用,比如fun()),严格模式默认undefined,非严格模式默认globalThis,这被称为默认绑定。
其他绑定方式还有
- 作为对象的方法,函数内的this指向该对象,这被称为隐式绑定,注意两点,
- 绑定到最近的对象,比如o.a.b(),b函数绑定到a对象
- 如果将函数作为对象赋值给另一个变量,就会成为默认绑定,比如
var b=o.b;b(),这被称为绑定丢失
- call、apply绑定,用来将函数指定this并执行,在非严格模式下,作为this的参数如果不是对象会先转化为对象,其中null和undefined会转换为globalThis,其他原始值会使用相应构造函数转换为对象
- bind,调用
f.bind(someObject)会返回一个this指向someObject的函数,且只能bind调用一次 - new调用,其中的this指向返回的实例对象,如果返回的是另一个对象,则其中的this无效。以上绑定方法中new调用优先级最高,其次是不同call,apply和bind绑定,然后是隐式绑定最后是默认绑定
- 箭头函数中的this和外部上下文的this指向相同,不能用call,apply和bind绑定方式绑定,其中的this参数会被忽略。
- 作为dom事件处理函数,this指向触发事件的元素
- 作为内联事件处理函数,this指向监听器所在的domt元素
class上下文
类本质上就是函数,也有一些区别。
类中的所有非静态方法会添加到this上,派生类的构造函数没有初始的this绑定,调用super才会进行。
另外类中的函数和其他普通函数一样,方法中的this取决于它们如何被调用,因此有必要在构造函数中进行绑定。
class Car {
constructor() {
// Bind sayBye but not sayHi to show the difference
this.sayBye = this.sayBye.bind(this);
}
sayHi() {
console.log(`Hello from ${this.name}`);
}
get name() {
return 'Ferrari';
}
}
new
可以使用new操作符实例化一个构造函数或类
new constructor[([arguments])]
new关键字执行时会进行如下操作
- 创建一个空对象,即{}
- 设置该对象的[[prototype]]为被实例化的构造函数或类
- 将这个新对象作为this的上下文
- 如果被调用的构造函数没返回对象,则返回this
function CNew() {
const [ctr, ...params] = Array.from(arguments);
const obj = {};//1
Object.setPrototypeOf(obj, ctr.prototype);//2
const result = ctr.apply(obj, params);//3
return typeof result === "object" ? result : obj;//4
}
递增和递减
- A++/A-- 先返回再加1/减1
- ++A/--A 先加1/减1后返回
一元运算符
- delete 移除对象上的一个属性,如果这个属性没有其他引用则将被释放
- void 丢弃一个表达式的返回值然后返回undefined,小括号可选,可以用于
javascript:协议,比如
<a href="javascript:void(document.body.style.backgroundColor='green');">
Click here for green background
</a>
- typeof 返回一个字符串表示参数的类型,小括号可选,其中原始类型中null返回object,其他返回对应类型;对象中函数对象返回function,其他返回object。
- + 一元加号用于将操作数转化为所代表的数字,此外无其他操作
- - 一元减,转化为number,然后取相反数
逻辑操作符
- ! 逻辑非
- && 逻辑与,
expr1 && expr2中,expr1可以转化为true则返回expr2,否则返回expr1,比后面的||先运算 - || 逻辑或
expr1 || expr2中,expr1可以转化为true则返回expr1,否则返回expr2 - ?? nullish coalescing操作符,如果左操作数是null or undefined,则返回右操作数,否则返回左操作数
数学操作
返回计算结果的数字
- +
- -
- /
- *
- % 取余
- ** 指数操作
关系操作
返回布尔值表示是否存在特定关系
- in 一个对象中是否有指定属性 包括自身的和继承的
- instanceof 表示前者的原型链中有没有出现过后者的prototype属性
- < 小于,比较的基本步骤如下
- 如果是对象则调用
Symbol.ToPrimitive,其hint参数为'number',转化为number - 如果两个都是字符串,则按字符串基于各自的unicode码点比较
- 否则将非数字类型的值转化为数字类型
- 如果存在NaN则返回false
- 否则比较数字大小
- 如果是对象则调用
- > 大于,比较步骤参考小于
- <=
- >=
求等
返回布尔值,表示是否相等
- == 如果两个操作数不同类型会试图转换类型再比较,具体步骤为
- 如果两个操作数都是对象,则指向一个引用才相等
- 如果一个是null另一个是undefined,则相等
- 如果类型不一样,比较之前要转换
- 比较数字和字符串,会将字符串转换为数字
- 如果存在布尔值,则转换为数字
- 如果一个是对象,一个是字符串或者数字,则使用对象的valueOf() and toString()转换为原始类型
- 如果两个操作数是同一类型
- 字符串,相同字符才相等
- 数字,±0相等,NaN和其他数字不相等
- 布尔值
- != 和==相反
- === 严格相等,不同类型的会被认为不同,其他和==一样
- !== 和严格相等相反
位操作符
位运算操作数接受32位整数,操作数中超过32位的会被丢弃。运算时会用有符号32位整数的补码表示,补码是符号位不变,其他位取反后加1。因此一个数字运算时需要先截取32位,然后计算其补码运算,运算结束再换算回来。
表示范围,最大值,符号位为0,其他位为1,即0x7fffffff,2147483647,最小值,符号位为1,为了使数值位最大,因此补码数值位应最小,数值位全是0,计算代表什么数时,无法直接减1取反,因此先加1,再减一取反,即-(2^31-1),再减去刚才加的1,结果是-2^31,即-2147483648
补码计算方式为
- 原码,直接转换成二进制后,前面补0,最高位为符号位,正数为0,负数为1 如转化为8位二进制,11的原码为00001011,-11原码为10001011。
- 反码,负数除符号位所有位取反,正数不变
- 补码,负数反码+1,正数不变 位操作符包括两种,一种是逻辑操作符一种是移动操作符
逻辑操作符
- ~ 按位取反,对每一位执行非操作,最终结果是~x=-(x + 1)
9 (base 10) = 00000000000000000000000000001001 (base 2)
--------------------------------
~9 (base 10) = 11111111111111111111111111110110 (base 2) = -10 (base 10)
- & 按位与,对每对比特位执行与操作,当两者都是1时才是1
9 (base 10) = 00000000000000000000000000001001 (base 2)
14 (base 10) = 00000000000000000000000000001110 (base 2)
--------------------------------
14 & 9 (base 10) = 00000000000000000000000000001000 (base 2) = 8 (base 10)
- | 按位或,对每一对比特位执行或操作,当两者都为0才是0,比如x|0表示x取整
9 (base 10) = 00000000000000000000000000001001 (base 2)
14 (base 10) = 00000000000000000000000000001110 (base 2)
--------------------------------
14 | 9 (base 10) = 00000000000000000000000000001111 (base 2) = 15 (base 10)
- ^ 按位亦或,每对不同时为1
9 (base 10) = 00000000000000000000000000001001 (base 2)
14 (base 10) = 00000000000000000000000000001110 (base 2)
--------------------------------
14 ^ 9 (base 10) = 00000000000000000000000000000111 (base 2) = 7 (base 10)
移动操作符
两个操作符分别表示被操作数和移动的长度
- << 左移操作符 左移超过的比特位会被丢弃,右侧用0补充,x<<y=x * 2 ** y.
9 (base 10): 00000000000000000000000000001001 (base 2)
--------------------------------
9 << 2 (base 10): 00000000000000000000000000100100 (base 2) = 36 (base 10)
- >> 右移操作符 右移超过的会被丢弃,复制最左边的比特位补充,由于符号位不变因此称为符号传播
. 9 (base 10): 00000000000000000000000000001001 (base 2)
--------------------------------
9 >> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)
-9 (base 10): 11111111111111111111111111110111 (base 2)
--------------------------------
-9 >> 2 (base 10): 11111111111111111111111111111101 (base 2) = -3 (base 10)
- >>> 无符号右移 右移超过的会被丢弃,左侧补0,因此符号位会变成0,正数和一般右移相同
9 (base 10): 00000000000000000000000000001001 (base 2)
--------------------------------
9 >>> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)
-9 (base 10): 11111111111111111111111111110111 (base 2)
--------------------------------
-9 >>> 2 (base 10): 00111111111111111111111111111101 (base 2) = 1073741821 (base 10)
条件运算符
- (condition ? ifTrue : ifFalse)
Optional Chaining运算符
- ?. 如果引用是null或undefined,不报错而是返回undefined
obj.val?.prop
obj.val?.[expr]
obj.arr?.[index]
obj.func?.(args)
赋值运算符
- =
- *=
- **=
- /=
- %=
- +=
- -=
- <<=
- >>=
- >>>=
- &=
- ^=
- |=
- &&=
- ||=
- ??=
- [a, b] = [1, 2] {a, b} = {a:1, b:2} 解构赋值,通过解构赋值可以将值从数组、属性从对象中取出到其他变量,语法
let a, b, rest;
[a, b] = [10, 20];
console.log(a); // 10
console.log(b); // 20
[a, b, ...rest] = [10, 20, 30, 40, 50];
console.log(a); // 10
console.log(b); // 20
console.log(rest); // [30, 40, 50]
({ a, b } = { a: 10, b: 20 });
console.log(a); // 10
console.log(b); // 20
// Stage 4(finished) proposal
({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40});
console.log(a); // 10
console.log(b); // 20
console.log(rest); // {c: 30, d: 40}
可以使用默认值
var {a = 10, b = 5} = {a: 3};
console.log(a); // 3
console.log(b); // 5
新的变量名
var {a:aa = 10, b:bb = 5} = {a: 3};
console.log(aa); // 3
console.log(bb); // 5
逗号运算符
- , 连接多个表达式分别计算,并返回最后一个
13. 语句和声明
js程序包含多个正确语法的语句,一个语句可能跨多行,多个语句也可能通过分号隔离写在一行。
控制流
- block 一个块语句用来包含0个或多个语句,用
{}包含。通常和if...else等结合使用,当使用let,const或严格模式下单独的块会有块作用域,否则依然是全局作用域。
可以带label用于识别或者作为break掉对应的块
LabelIdentifier: {
StatementList
}
- break 终止当前的loop、switch或label声明,然后将程序的控制权转移到其他语句
break [label];
当使用label时需要嵌套在对应label中,label可以指普通的块语句。
outer_block:{
inner_block:{
console.log ('1');
break outer_block; // breaks out of both inner_block and outer_block
console.log (':-('); // skipped
}
console.log ('2'); // skipped
}
- continue 停止当前loop或者带标签loop的当前迭代,继续下一次迭代
continue [label];
比如
let text = '';
for (let i = 0; i < 10; i++) {
if (i === 3) {
continue;
}
text = text + i;
}
console.log(text);
// expected output: "012456789"
- Empty 空语句就是没有语句,只有一个分号
let arr = [1, 2, 3];
// Assign all array values to 0
for (let i = 0; i < arr.length; arr[i++] = 0) /* empty statement */ ;
console.log(arr);
// [0, 0, 0]
- if...else
- switch
- throw
- try...catch
try {
try_statements
}
[catch [(exception_var)] {
catch_statements
}]
[finally {
finally_statements
}]
声明
变量命名
变量名有两个限制
- 只能包含字母,数字,符号 $ 和 _
- 首字符不能是数字
变量创建过程
包括创建、初始化和赋值三个步骤
var a=1
这个例子里,创建指的是创建了a这个变量,但是没有保存任何值,因此不能使用;初始化指的是变量在赋值之前会初始化为undefined;赋值就是手动给变量赋值为1.
声明方法
声明变量可以用var,let,const,他们有以下不同
- var 旧时的声明方式,创建和初始化会提前,弃用
- let 和var类似,但有以下不同
- 只在代码块内部有效
- 创建会提升,因此在let之前不能任何操作
- 不允许重复声明
- const 和let类似,只是声明后必须初始化且不能改
创建一个函数分为函数表达式和函数声明,函数声明使用function关键字,函数表达式就是声明一个变量保存函数。 当使用function函数声明一个函数时,变量创建三步骤都会被提升
迭代
- do...while 执行一个语句直到条件是false,至少会执行一次
do
statement
while (condition);
- for 包含三个可选的用逗号分隔的表达式,后面跟了个语句
for ([initialization]; [condition]; [final-expression])
statement
其中初始化表达式中的变量声明,如果用let,被声明的变量则为后面语句的本地变量
- for...in 迭代一个对象的可枚举属性,包括继承的,不包括symbol属性
for (variable in object)
statement
var obj = {a: 1, b: 2, c: 3};
for (const prop in obj) {
console.log(`obj.${prop} = ${obj[prop]}`);
}
// Output:
// "obj.a = 1"
// "obj.b = 2"
// "obj.c = 3"
- for...of 迭代一个可迭代对象(包括 arrays, array-like objects, iterators and generators)
for (variable of iterable) {
statement
}
const iterable = [10, 20, 30];
for (const value of iterable) {
console.log(value);
}
// 10
// 20
// 30
- for await...of 迭代一个异步迭代对象,只能在async function 中使用
var asyncIterable = {
[Symbol.asyncIterator]() {
return {
i: 0,
next() {
if (this.i < 3) {
return Promise.resolve({ value: this.i++, done: false });
}
return Promise.resolve({ done: true });
}
};
}
};
(async function() {
for await (num of asyncIterable) {
console.log(num);
}
})();
// 0
// 1
// 2
- while
while (condition)
statement
其他
- debugger
- export
- label 给语句一个标识符,用于break和continue
- with 为语句扩展作用域,在性能和语义方面存在问题,在严格模式禁止使用
14. Function和class
函数
可以说,一个函数相当于一个子程序,可以被外部的代码调用,也可以在内部递归调用。就像一个程序本身,一个函数也是由一系列语句组成,即函数体。
在js中,函数是一等对象,因为既可以像其他对象一样有属性和方法,而且可以被调用
定义函数
- 函数声明(或者叫函数语句),会提升
function name([param[, param[, ... param]]]) {
statements
}
- 函数表达式,语法很像函数声明,可以用来定义匿名函数或者具名函数,不会提升。
function [name]([param[, param[, ... param]]]) {
statements
}
var myFunction = function() {
statements
}
如果函数只用一次,可以使用 IIFE (Immediately Invoked Function Expression)
(function() {
statements
})();
- generator函数声明(或者叫function*语句)
function* name([param[, param[, ... param]]]) {
statements
}
- generator函数表达式(function* expression)
function* [name]([param[, param[, ... param]]]) {
statements
}
- 箭头函数表达式
([param[, param]]) => {
statements
}
param => expression
- Function构造函数,不推荐直接使用,因为函数体是字符串,会阻止js引擎优化
new Function (arg1, arg2, ... argN, functionBody)
- GeneratorFunction 构造函数
new GeneratorFunction (arg1, arg2, ... argN, functionBody)
arguments对象
是一个类数组的对象,可以在函数体访问,包含传给函数的参数,包含以下属性
- arguments.callee 表示参数属于的函数,在严格模式禁止使用
- arguments.length 传到参数的长度
- arguments[@@iterator] 返回迭代器
函数默认参数允许没有值或者undefined传入时使用默认形参。
function [name]([param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]]) {
statements
}
默认参数可以用后面的默认参数
function greet(name, greeting, message = greeting + ' ' + name) {
return [name, greeting, message];
}
greet('David', 'Hi'); // ["David", "Hi", "Hi David"]
greet('David', 'Hi', 'Happy Birthday!'); // ["David", "Hi", "Happy Birthday!"]
有默认值的结构参数
function f([x, y] = [1, 2], {z: z} = {z: 3}) {
return x + y + z;
}
f(); // 6
rest参数语法使我们将一个不定数量的参数表示为一个数组
function(a, b, ...theArgs) {
// ...
}
对象上的方法
var obj = {
property( parameters… ) {},
*generator( parameters… ) {},
async property( parameters… ) {},
async* generator( parameters… ) {},
// with computed keys:
[property]( parameters… ) {},
*[generator]( parameters… ) {},
async [property]( parameters… ) {},
// compare getter/setter syntax:
get property() {},
set property(value) {}
};
其中set语法会在设置对应属性时调用,get在获取对应属性时调用。
箭头函数
是传统函数表达式的一种变体,但是使用起来会有一些限制
- 不能使用构造函数
- 不能绑定到this,super
- 不能使用arguments或new.target关键字
- 不适用apply,call或bind方法
- 不能使用yield
类
class是一个创建对象的模板,js中的class基于原型,但也有另外的一些用法。
定义class
和函数一样,也有两种定义方式,class表达式或class声明。
class声明
不会提升
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class表达式
// unnamed
let Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// output: "Rectangle"
// named
let Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// output: "Rectangle2"
class body和方法定义
- class的body在严格模式下执行
- constructor,该方法是个特殊方法,用于创建和初始化一个class创建的对象,一个类只能有一个,可以使用super调用父类的构造函数。
- 原型方法,参考函数部分中,对象的方法
class Rectangle {
// constructor
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea()
}
// Method
calcArea() {
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square.area);
// 100
- 公共、私有,默认公有,私有添加井号
- 实例、静态,默认实例,静态添加static,同时静态私有写法为static #staticFieldPrivateName
- 字段可以直接在顶级声明而不需要在构造函数中 这一块具体可参考这篇关于es2022的文章
class Rectangle {
#height = 0;
width;
constructor(height, width) {
this.#height = height;
this.width = width;
}
}
子类
使用extends扩展
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // call the super class constructor and pass in the name parameter
}
speak() {
console.log(`${this.name} barks.`);
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.
也可以扩展常规方法声明的构造函数。
Species
用来自定义内置构造函数,比如
class MyArray extends Array {
// Overwrite species to the parent Array constructor
static get [Symbol.species]() { return Array; }
}
let a = new MyArray(1,2,3);
let mapped = a.map(x => x * x);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
Mix-ins
是class的模板.一个class只能有一个父类,因此一个mix-in需要提供父类
let calculatorMixin = Base => class extends Base {
calc() { }
};
let randomizerMixin = Base => class extends Base {
randomize() { }
};
class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }
15. Scripts and Modules
script是相对于module的一个概念,两者主要有以下区别。
| - | Scripts | Modules |
|---|---|---|
| HTML element | <script> | <script type="module"> |
| Default mode | non-strict | strict |
| Top-level variables are | global | local to module |
| Value of this at top level | window | undefined |
| Declarative imports (import statement) | no | yes |
| Programmatic imports (Promise-based API) | yes | yes |
| File extension | .js | .js |
这里主要是讲module相关,参考Modules和ES modules: A cartoon deep-dive。
背景
我们写代码的过程就是管理变量的过程,当项目变得比较大时怎么维护这些变量就产生了问题。在js中有作用域得概念可以帮我们维护代码,我们在之前的script中使用的顶级变量基本是在全局作用域,这种用法存在一些问题
- 不同的script引入需要保证顺序
- 不同文件的依赖不易分清
- 全局作用域的变量难以维护
另外可以使用函数作用域定义不同变量,但是在函数作用域声明的变量难以被外界访问。
于是我们可以引入模块,我们可以将代码分成很多块各自放到一个模块中,使每个模块维护自己作用域的变量,然后通过显式地导出和导入对特定变量进行共享,依赖关系也变得清晰。
工作流程
模块的使用经历以下三个步骤
- Construction 构建阶段需要从入口文件开始
- 进行模块解析找到依赖的模块
- 加载模块
- 将所有模块解析成一个个数据结构,即Module Records
- Instantiation 实例化,一个模块实例包括代码和数据两部分,实例化过程将Module Records组合,并将导出导入的变量和内存中的地址对应。注意这一点和commonjs的实现不同,后者是引入的副本,这意味着导出的模块后期对对应变量的修改,引入的模块是无感知的;而在ES module可以随时对导出的变量修改,引入的文件不可以修改这些值,但是可以修改引入对象上的属性。
- Evaluation 计算,执行顶层代码将填充对应变量的内存地址,每个模块只执行一遍。
正是由于ES module中对变量的live bindings,可以很好的处理循环引用,比如
在这个例子中使用commonjs,从main.js开始执行,执行到require语句加载counter模块,后者试图message变量,此时为undefined,等counter文件执行完回到main文件继续执行,后续即使message有了值,在count中引入的变量依然是undefined,但是如果是ES module,counter模块最终会获得正确的值。
使用
js原生的模块使用其实和我们平时使用各种第三方工具实现的很像,这里大致过一下
export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果希望外部文件能够读取该模块的变量,就需要在这个模块内使用export关键字导出变量。如:
// profile.js
export var a = 1;
export var b = 2;
export var c = 3;
下面的写法是等价的,这种方式更加清晰(在底部一眼能看出导出了哪些变量):
var a = 1;
var b = 2;
var c = 3;
export {a, b, c}
export命令除了输出变量,还可以导出函数或类。
导出函数
export function foo(){}
function foo(){}
function bar(){}
export {foo, bar as bar2}
其中上面的as表示给导出的变量重命名。 要注意的是,export导出的变量只能位于文件的顶层,如果处于块级作用域内,会报错。如:
function foo() {
export 'bar'; // SyntaxError
}
导出类
export default class {} // 关于default下面会说
export语句输出的值是动态绑定,绑定其所在的模块。
// foo.js
export var foo = 'foo';
setTimeout(function() {
foo = 'foo2';
}, 500);
// main.js
import * as m from './foo';
console.log(m.foo); // foo
setTimeout(() => console.log(m.foo), 500); // foo2
import
import命令可以导入其他模块通过export导出的部分。
// abc.js
var a = 1;
var b = 2;
var c = 3;
export {a, b, c}
//main.js
import {a, b, c} from './abc';
console.log(a, b, c);
如果想为导入的变量重新取一个名字,使用as关键字(也可以在导出中使用)。
import {a as aa, b, c};
console.log(aa, b, c)
如果想在一个模块中先输入后输出一个模块,import语句可以和export语句写在一起。
import {a, b, c} form './abc';
export {a, b, c}
// 使用连写, 可读性不好,不建议
export {a, b, c} from './abc';
模块的整体加载 使用*关键字。
// abc.js
export var a = 1;
export var b = 2;
export var c = 3;
// main.js
import * from as abc form './abc';
console.log(abc.a, abc.b, abc.c);
export default
在export输出内容时,如果同时输出多个变量,需要使用大括号{},同时导入也需要大括号。使用export defalut输出时,不需要大括号,而输入(import)export default输出的变量时,不需要大括号。
// abc.js
var a = 1, b = 2, c = 3;
export {a, b};
export default c;
import {a, b} from './abc';
import c from './abc'; // 不需要大括号
console.log(a, b, c) // 1 2 3
本质上,export default输出的是一个叫做default的变量或方法,输入这个default变量时不需要大括号。
// abc.js
export {a as default};
// main.js
import a from './abc'; // 这样也是可以的
// 这样也是可以的
import {default as aa} from './abc';
console.log(aa);
动态导入
返回一个promise
import('/modules/my-module.js')
.then((module) => {
// Do something with the module.
});
16. 错误处理和语言扩展
这里讲的是在语言实现方面的一些注意,这里不展开。
17. 标准内置对象
每当script或module执行时都会有可用的内置对象,一个是全局对象,即globalThis,一个是其他内置的其他全局的对象,其他的可以通过全局对象的初始属性或者间接作为内置对象的属性访问。
18. 全局对象
全局对象符合以下特征
- 会在进入任何执行上下文之前创建
- 没有[[call]]内置方法,不能作为函数调用
- 有一个 [[Prototype]]内置钩子,其是宿主定义的
- 可能有一些规范之外,由宿主定义的属性,可能也包括一个属性是全局对象自身。
这里只介绍全局对象值属性和函数属性,构造函数属性和其他属性在后面章节介绍。
全局对象的值属性
- globalThis 是全局的this值。在此之前在不同的宿主环境访问this需要不同的语法,比如window或global等。
- Infinity 即Number.POSITIVE_INFINITY,正无穷
- NaN 即 Number.NaN,not a number
- undefined 表示原始值undefined,一个变量没有赋值就是undefined,一个方法或语句默认返回undefined
全局对象的函数属性
- eval 执行一个字符串代码,其不安全且性能不好,不要使用
- isFinite()
- isNaN()
- parseFloat() 将字符串参数转化为浮点数,如果不是字符串需要先转化为字符串。
- parseInt(string, radix)
- URI编解码
- encodeURI()/decodeURI()
- encodeURIComponent()/decodeURIComponent() 两者使用效果如下
var set1 = ";,/?:@&=+$"; // 保留字符
var set2 = "-_.!~*'()"; // 不转义字符
var set3 = "#"; // 数字标志
var set4 = "ABC abc 123"; // 字母数字字符和空格
console.log(encodeURI(set1)); // ;,/?:@&=+$
console.log(encodeURI(set2)); // -_.!~*'()
console.log(encodeURI(set3)); // #
console.log(encodeURI(set4)); // ABC%20abc%20123 (the space gets encoded as %20)
console.log(encodeURIComponent(set1)); // %3B%2C%2F%3F%3A%40%26%3D%2B%24
console.log(encodeURIComponent(set2)); // -_.!~*'()
console.log(encodeURIComponent(set3)); // %23
console.log(encodeURIComponent(set4)); // ABC%20abc%20123 (the space gets encoded as %20)
前者因为无法对一些符号(比如"&", "+", 和 "=")编码,无法产生适合http请求的url,要使用后者。
19. 基本对象
是其他对象的基础
Object
几乎所有对象(除了使用Object.create(null)创建,或者被Object.setPrototypeOf修改后的)都是Object的实例,一个特定对象会从Object.prototype上继承属性,其中某些属性可能会被重写。
对象可以使用构造函数或者字面量创建。
对象中没有特定方法删除属性,可以使用delete操作符
构造函数
new Object()
new Object(value)
当参数是undefined或null时返回空对象,否则返回对应的对象类型。如果本身就是对象则直接返回。
静态方法
原型相关
- Object.create(proto, [propertiesObject]) 用一个存在的对象作为新创建对象的原型,第二个参数相当于
Object.defineProperties()的第二个参数,用来指定属性描述符。 - Object.getPrototypeOf() 返回[[Prototype]]属性
- Object.setPrototypeOf(obj, prototype) 设置[[Prototype]]属性
属性相关
- Object.assign(target, ...sources) 从一个或多个源对象复制可枚举自身属性到目标对象,返回目标对象
- Object.defineProperty(obj, prop, descriptor) 在一个对象上定义一个新属性或者修改已经存在的属性,然后将该对象返回
- Object.defineProperties(obj, props) 在一个对象上定义或重写对象,返回该对象
- Object.getOwnPropertyDescriptor(obj, prop) 返回对象指定属性描述符
- Object.getOwnPropertyDescriptors(obj) 返回对象的所有属性描述符 迭代相关
- Object.entries(obj) 返回给定对象自身可枚举的键值对数组([key, value][])
- Object.fromEntries()
Object.entries的逆运算 - Object.getOwnPropertyNames(obj) 返回自身的枚举和不可枚举属性组成的数组,不包含symbol属性
- Object.getOwnPropertySymbols(obj) 返回自身的symbol属性组成的数组
- Object.keys(obj) 返回自身可枚举属性组成的数组
- Object.values(obj) 返回自身可枚举属性值组成的数组
属性冻结相关
- Object.freeze(obj) 对象的任何属性都不能修改,如果对象的属性是对象则该属性可以修改除非它也是个冻结对象,返回冻结后的该对象,影响原型链
- Object.preventExtensions(obj) 使不可扩展,即不可添加属性,不影响原型链
- Object.seal(obj) 和Object.freeze()相比,现存属性可以修改
- Object.isExtensible(obj)
- Object.isFrozen(obj)
- Object.isSealed(obj) 以上冻结程度freeze>seal>Extension
判等
- Object.is(value1, value2) 判断两个值是否相等,和===的区别是,前者区分了±0,并且认为NaN和本身相等
实例属性
- constructor 除了
Object.create(null)生成的以外,其余对象都有这个属性,指向对应构造函数。该属性可以修改。如果由Object.create创建的对象,则其该属性是参数的constructor,比如
function Parent() {}
Parent.prototype.parentMethod = function parentMethod() {};
function Child() {}
Child.prototype = Object.create(Parent.prototype); // re-define child prototype to Parent prototype
Child.prototype.constructor = Child; // 默认是Parent.prototype.constructor,即function Parent() {}
- __proto__ 仅作了解,不要使用
实例方法
- obj.hasOwnProperty(prop) 检查属性是否为自身的,包括symbol属性
- prototypeObj.isPrototypeOf(object) 检查prototypeObj是否在object原型链上,区别于
"object instanceof AFunction"表示AFunction.prototype是否在object原型链上。 - obj.propertyIsEnumerable(prop) 返回对应属性是不是自身可枚举的属性
- object.valueOf() 返回对象的原始值,没有对应原始类型的会返回本身,Date返回毫秒数
- obj.toString() 返回对象的字符串表示,默认是
[object type]的格式,其中type是对象的类型 - obj.toLocaleString() 返回调用toString()的结果,但是被很多对象重写了
Function
每个函数都是一个Function对象
实例属性
- name 函数的名字
- length 参数的个数
实例方法
- func.apply(thisArg, [ argsArray]) 用一个给定的this调用函数,参数以数组的形式提供
- func.call([thisArg[, arg1, arg2, ...argN]]) 和apply的区别是参数参数列表而不是数组
- let boundFunc = func.bind(thisArg[, arg1[, arg2[, ...argN]]]) 创建一个新函数,this为指定的参数,另外的参数是作为新函数预制的参数
- function.toString() 返回函数的源码
Boolean
是布尔值的对象包装,toString返回带引号的布尔值
Symbol
symbol是一种一种原始类型,调用Symbol([description])创建,每个返回值都是唯一的,其中参数是字符串,是对symbol的描述,即使参数相同,返回值也是唯一的。
全局symbol
利用Symbol.for(key)方法和Symbol.keyFor(sym)可以在全局注册symbol,前者查找对应键的symbol,如果找到则使用否则注册,返回symbol;后者根据指定symbol返回指定键值,如果没找到则返回undefined.
查找symbol属性
Object.getOwnPropertySymbols()
众所周知的symbol
symbol有一些静态属性,可以通过它们在一些内置对象中获取方法,这些方法会在内置对象执行特定方法时调用。下面是静态属性对应的操作 迭代
- Symbol.iterator for...of
- Symbol.asyncIterator for await of 正则表达式
- Symbol.match String.prototype.match()
- Symbol.replace String.prototype.replace()
- Symbol.search String.prototype.search()
- Symbol.split String.prototype.split() 其他
- Symbol.hasInstance instanceof
- Symbol.isConcatSpreadable Array.prototype.concat()
- Symbol.unscopables 拥有和继承属性名的一个对象的值被排除在与环境绑定的相关对象外。
- Symbol.species 一个用于创建派生对象的构造器函数。
- Symbol.toPrimitive 一个将对象转化为基本数据类型的方法。
- Symbol.toStringTag Object.prototype.toString()
参考
Error
运行时发生错误时抛出的对象,其作为基类,在js中还有其他很多错误类型
数字和日期
Number
关于数字类型的64位浮点格式的表示以及表示范围、精度计算方法等前面已经说了,这里不赘述。
数值字面量
- 十进制 最常用的进制,可以在最前面加0,但是如果所有数字都小于8,会被当做八进制
- Exponential指数表示,格式为
beN,其中b指底数,e是指数符号,N是指指数 - 二进制 以0b或0B开头
- 八进制 以0o或0O开头
- 十六进制 以0x或0X开头
静态属性
- Number.EPSILON 代表比1大的最小浮点数和1之间的差
- 安全整数,在这个范围内可以准确表示整数,比如
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2为true- Number.MAX_SAFE_INTEGER 最大
- Number.MIN_SAFE_INTEGER 最小
- 表示范围,
- Number.MAX_VALUE 最大正数
- Number.MIN_VALUE 最小正数
- Number.POSITIVE_INFINITY
- Number.NEGATIVE_INFINITY
- Number.NaN
静态方法
- Number.isNaN()
- Number.isFinite()
- Number.isInteger()
- Number.isSafeInteger()
- Number.parseFloat(string)
- Number.parseInt(string, [radix])
实例方法
- numObj.toExponential([fractionDigits]) 以指数表示法表示,用参数来制定小数点后几位数字。0到20之间,默认是根据数字保留尽量多的位数
- numObj.toFixed([digits]) 采用非指数的定点表示法,参数是小数点后保留几位,默认0
- numObj.toPrecision([precision]) 返回一个定点或者指数表示的字符串,参数表示精度,即用几个非0数字表示
- numObj.toString([radix]) 进制转换,参数是2到36之间的数字,返回对应进制的字符串
- numObj.toLocaleString([locales [, options]]) 返回数字特定语言环境下的字符串表示,其中参数用于自定义函数的行为,具体可参考Intl.NumberFormat() constructor
BigInt
是一个内置对象用来表示安全值以外的整数,用字面量表示时在数字后面加n
Math
是一个内置对象,其有一些属性和方法表示数学常量和计算,不能用于bigInt,这里只展示常用的
静态属性
- Math.PI 圆周率
对数相关
- Math.E 自然对数的底数,ln
- Math.LN2 2的自然对数
- Math.LN10 10的自然对数
- Math.LOG2E 以2为底e的对数
- Math.LOG10E 以10为底e的对数
平方根
- Math.SQRT1_2 ½ 的平方根
- Math.SQRT2 2的平方根
静态方法
- Math.abs(x) 绝对值
- Math.max([x[, y[, …]]]) 多个数值最大值
- Math.min([x[, y[, …]]]) 多个数值最小值
- Math.pow(x, y) x的y次方
- Math.random() 得到0到1之间(前闭后开)的伪随机数,不能提供安全的随机数
- Math.sqrt(x) 一个数的平方根
- Math.cbrt(x) 返回某数的立方为x
- Math.clz32(x) 32位整数前导0的数量
- Math.exp(x) e的x平方
- Math.fround(x) 最接近的单精度浮点数
- Math.imul(x, y) 32位整数相乘
- Math.log(x) 一个数的自然对数,相当于㏑
- Math.log1p(x) 一个数加 1 的和的自然对数
- Math.log10(x) 一个数以 10 为底数的对数
- Math.log2(x) 一个数以 2 为底数的对数 近似
- Math.ceil(x) 向上取整
- Math.floor(x) 向下取整
- Math.round(x) 四舍五入的整数
- Math.trunc(x) 返回整数部分
Date
表示从midnight on January 1, 1970, UTC到现在的毫秒数
构造函数
new Date()
new Date(value)
new Date(dateString)
new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]])
静态方法
- Date.now() 返回表示的毫秒数
- Date.parse(dateString) 解析一个代表日期的字符串返回毫秒数
- Date.UTC() 接受和构造函数相同的参数返回毫秒数
实例方法
年
- getFullYear() 4位数的年
- setFullYear()
月
- getMonth() 0-11的月份
- setMonth()
日
- getDate() 1到31
- setDate()
星期几
- getDay() 0到6 没有设置周几的方法
时
- getHours() 0到23
- setHours()
分
- getMinutes() 0到59
- setMinutes()
秒
- getSeconds() 0到59
- setSeconds()
毫秒
- getMilliseconds() 0到999
- setMilliseconds()
其他
- getTime() 时间戳
- getTimezoneOffset() utc相对于当前时区的时间差,单位mins,比如东八区为-480
- toDateString() 对应格式,比如Wed Jul 28 1993
- toISOString() 这种格式YYYY-MM-DDTHH:mm:ss.sssZ
- toJSON() 和toISOString()类似
- toLocaleDateString([locales [, options]]) 本地格式输出,参数参考
- toLocaleString([locales[, options]])
21. 文本处理
String
用来表示和操作字符序列
创建字符串
分为字面量和构造函数方法,字面量法三种表示,其中最后一种指定了模板字面量可以使用插值,构造函数法后面介绍
const string1 = "A string primitive";
const string2 = 'Also a string primitive';
const string3 = `Yet another string primitive`;
模板字面量
使用反引号代替引号,是允许嵌入表达式(${expression})的字符串字面量,可以使用多行和插值的功能,之前叫做模块字符串。
另外还有种用法叫做带标签的模板(Tagged templates),可以使用函数解析模板字符串,其中第一个参数是包含字符串值的数组,其余的是模板字符串中的插值,比如
var person = 'Mike';
var age = 28;
function myTag(strings, personExp, ageExp) {
var str0 = strings[0]; // "that "
var str1 = strings[1]; // " is a "
// There is technically a string after
// the final expression (in our example),
// but it is empty (""), so disregard.
// var str2 = strings[2];
var ageStr;
if (ageExp > 99){
ageStr = 'centenarian';
} else {
ageStr = 'youngster';
}
return str0 + personExp + str1 + ageStr;
}
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster
另外标签函数的第一个参数有一个属性row,我们可以访问模板字符串的原始字符串
function tag(strings) {
console.log(strings.raw[0]);
}
tag`string text line 1 \n string text line 2`;
// logs "string text line 1 \n string text line 2" ,
// including the two characters '\' and 'n'
字符访问
有两种方式访问字符串中的字符,第一种是charAt()
'cat'.charAt(1)
另一种是作为数组访问
'cat'[1]
比较字符串
直接可以用大于号或小于号比较,也可以使用referenceStr.localeCompare(compareString[, locales[, options]]),参数和语言环境有关,如果前面的字符比参数字符在前则返回负数
转义符
具体见Escape notation,比如
- \n 换行符
- \t 制表符
- \b 退格
- \r 回车
- \' 引号字符
- \xXX 以两个16进制表示一个字符 ascII码
- \uXXXX 以四个16进制表示一个字符 unicode
- \u{X…XXXXXX} 1到6个utf-32的unicode字符
静态方法
- String.fromCharCode(num1[, ...[, numN]]) 将一系列utf-16码点转化为字符串,范围是0到65535(0xFFFF)
- String.fromCodePoint(num1[, ...[, numN]]) 将码点转化为字符串的新方法,支持超过65535的码点,如果之前的方法想表示超过这个范围的字符就需要使用两个utf-16(被称为surrogate pair),比如
String.fromCharCode(0xD83C, 0xDF03);,而新的方法只需要String.fromCodePoint(0x1F303)
实例属性
不包含正则相关,其会在正则部分介绍
- 获取某位置上的UTF-16字符 str[pos]、str.charAt(pos),区别是如果没有找到字符,前者返回undefined,后者返回空字符串。
- 获取指定位置上对应的Unicode数字charCodeAt(index)和codePointAt(pos),其中后者可以识别四个字节表示的字符
- toLowerCase() 和 toUpperCase() 改变大小写
- 查找子字符串位置 str.indexOf(substr, pos)和str.lastIndexOf(substr, pos) 前者是从前往后查找,后者是从后往前查找,第二个参数是查找的起始位置
- 是否包含 str.includes(substr, pos), str.startsWith(substr, pos),str.endsWith(substr, length),分别是是否包含,是否在查找范围头部,是否为查找查找范围尾部,第二个参数可以进一步限制查找范围
- 获取子字符串
- str.slice(start [, end])返回字符串从 start 到(但不包括)end 的部分,如果位置是负数则从结尾算起
- str.substring(start [, end]) 和slice得区别是负数的话看作0,以大的数作为结尾
- str.substr(start [, length]) 从 start 开始获取长为 length 的字符串,start可以是负的
- str.concat(str2 [, ...strN]) 连接多个字符串文本,返回新的字符串
- 填充
- str.padEnd(targetLength [, padString])填充目标字符串尾部至目标长度,返回填充好的字符串,默认填充空格
- str.padStart(targetLength [, padString]) 填充头部
- 重复 str.repeat(count) 字符串重复count次
- 切分 str.split([separator[, limit]]) 以separator为分隔符切割成字符串数组,如果以空字符串为分隔,就会在utf-16之间分隔,从而毁坏两个utf-16编码组成的字符
- toString() 重写的Object.prototype.toString
- 去除空格 trim()、trimStart()、trimEnd()
- valueOf() 重写的Object.prototype.valueOf
- str[Symbol.iterator]() 返回迭代器对象
RegExp
即regular expressions,用来匹配一个模式的字符串
字面量和构造函数
字面量表示的参数在两个斜线之间,没有引号,该表达式只能是常量 构造函数的参数可以用斜线或引号,可以包含变量
let re = /ab+c/i; // literal notation
let re = new RegExp('ab+c', 'i') // constructor with string pattern as first argument
let re = new RegExp(/ab+c/, 'i') // constructor with regular expression literal as first argument (Starting with ECMAScript 6)
构造函数
/pattern/flags
new RegExp(pattern[, flags])
RegExp(pattern[, flags])
Parameters
其中patten是正则表达式文本,flag用来指导匹配过程,可以取值
- g (global match) 指定要在字符串中搜索所有的可能性,如果和y一起用,g将被忽略
- i (ignore case) 如果flag
u可用,则忽略大小写 - m (multiline) 将开始和结束符号(
^and$)视为多行工作,即匹配每行的开始和结尾,而不是整个输入的开头和结尾 - s ("dotAll") 使用
.匹配 \n, \r, \u2028 or \u2029,否则不能匹配 - u (unicode) 将patten作为一系列Unicode code points,,用来识别大于2个字节的码点 可以结合\p来匹配
//被匹配字符为四字节的一个字符,没有u会识别成两个字符
/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true
- y (sticky) 只从lastIndex开始匹配,不从后续索引匹配,比如
var str = '#foo#';
var regex = /foo/y;
regex.lastIndex = 1;
regex.test(str); // true (译注:此例仅当 lastIndex = 1 时匹配成功,这就是 sticky 的作用)
regex.lastIndex = 5;
regex.test(str); // false (lastIndex 被 sticky 标志考虑到,从而导致匹配失败)
regex.lastIndex; // 0 (匹配失败后重置)
静态属性
- Symbol.species 返回正则构造函数默认的构造器,子类可以重写这个属性
class MyRegExp extends RegExp {
// Overwrite MyRegExp species to the parent RegExp constructor
static get [Symbol.species]() { return RegExp; }
}
- lastIndex 在使用g或y修饰符时设置,表示下一次开始匹配的下标,当 exec() 或 test() 找不到可匹配的字符串时会将其设置为0
实例属性
- RegExp.prototype.flags 包含所有flags的字符串
- RegExp.prototype.dotAll
- RegExp.prototype.global
- RegExp.prototype.ignoreCase
- RegExp.prototype.multiline
- RegExp.prototype.sticky
- RegExp.prototype.unicode
- RegExp.prototype.source 返回表达式文本,即
\\之间的内容
正则语法
单字符
- . 有以下意义,在es2018中的
s修饰符允许匹配换行符- 表示出换行符(\n, \r, \u2028 or \u2029)之外的任何单个字符
- 在字符集(中括号包围的character set)中只表示字面意思
- \d 匹配任何数字,相当于[0-9]
- \D 非数字,相当于[^0-9]
- \w 任何数字字母和下划线,相当于[A-Za-z0-9_]
- \W 相当于[^A-Za-z0-9_]
- \s 匹配单个空格,包括 space, tab, form feed, line feed, and other Unicode spaces,相当于[ \f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
- \S 非空格
- \xhh 使用两个16进制(hexadecimal digits)匹配字符
- \uhhhh 使用四个utf-16匹配
- \u{hhhh} or \u{hhhhh} 只能用
u修饰符时使用
边界
- ^ 匹配输入的开始,如果有
m修饰符也会匹配紧跟换行符的字符 - $ 匹配输入的结束,如果有
m修饰符也会匹配换行符前面的字符 - \b 匹配单词边界,表示一个位置,其前面或后面没有字符,比如字母和空格之间。注意单词边界的长度为0
- \B 匹配非单词边界,表示一个位置,其前后要么都是单词要么都是非单词
向前或向后断言
- x(?=y) 向前断言,匹配跟着y的x
- x(?!y) 向后负断言,匹配没跟着y的x
- (?<=y)x 向前断言 匹配被y跟着的x
- (?<!y)x 向前负断言,匹配没被y跟着的x
分组和区间
- x|y 匹配x或y
- [xyz]或[a-c] 字符集,匹配其中任意一个字符,可用中划线表示的区间表示
- [^xyz]或[^a-c]
- (x) 捕获组,匹配x并且记住这个匹配。一个正则表达式可能有多个捕获组,匹配的捕获组会按照左小括号出现的顺序保存,可以通过下标[n]或者$n访问,其中n是正整数
- \n n是正整数,表示第几个捕获组的子字符串,比如/apple(,)\sorange\1/ matches "apple, orange," in "apple, orange, cherry, peach".
- (?<Name>x) 命名捕获组,将捕获的x分组命名为Name
- \k<Name> 按照名字引用捕获组,比如/(?\w+), yes \k/ matches "Sir, yes Sir" in "Do you copy? Sir, yes Sir!"
- (?:x) 非捕获组,使用小括号但是不捕获该捕获组
重复的次数
- x* 0或多次
- x+ 1或多次
- x? 0或1次
- x{n} n次
- x{n,} n是正整数,至少n次
- x{n,m} 至少n最多m次
- 次数后面加
?,比如x*? 默认是贪婪匹配,尽量匹配多的字符,加上问号后会不贪婪
转义
如果想使用任何特殊字符,需要加\转义
具体使用
包括 RegExp 的 exec 和 test 方法以及 String 的 match、replace、search 和 split
想知道是否匹配上,用test或search,需要知道更多信息用exec或match,replace用于替换子元素,split用于分隔字符串
- regexObj.test(str) 检查匹配,返回布尔值,如果正则表达式设置了全局标志,test() 的执行会改变正则表达式 lastIndex属性。连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串
- str.search(regexp) 返回第一次匹配的下标,如果查不到返回-1,如果参数不是正则则会转为正则,不支持global
- regexObj.exec(str) 执行一个搜索,返回结果数组(还有另外的index和input属性,命名捕获组group)或者null ,其中数组中第0项是完整的匹配字符串,其他是按顺序的捕获组,index是匹配到的字符串下标。
const reg = /(\d+)(\+)/
const str = '44454+2+11'
console.log(reg.exec(str))
//
[
'44454+',
'44454',
'+',
index: 0,
input: '44454+2+11',
groups: undefined
]
当正则表达式使用 "g" 标志时,可以多次执行 exec 方法来查找同一个字符串中的成功匹配。当你这样做时,查找将从正则表达式的 lastIndex 属性指定的位置开始。
const reg = /(\d+)(\+)/g
const str = '44454+2+11'
console.log(reg.exec(str))
console.log(reg.exec(str))
console.log(reg.exec(str))
//
[
'44454+',
'44454',
'+',
index: 0,
input: '44454+2+11',
groups: undefined
]
[ '2+', '2', '+', index: 6, input: '44454+2+11', groups: undefined ]
null
- str.match(regexp) 如果匹配到返回值取决于g修饰符,如果没匹配到返回null。
- 如果没使用,返回结果和exec不使用g一样
const reg = /(\d+)(\+)/ const str = '44454+2+11' console.log(str.match(reg)) //// [ '44454+', '44454', '+', index: 0, input: '44454+2+11', groups: undefined ]- 如果使用了g,所有完整匹配到的结果都会作为数组元素返回,没有另外的属性
const reg = /(\d+)(\+)/g const str = '44454+2+11' console.log(str.match(reg)) //[ '44454+', '2+' ] - str.matchAll(regexp) 一定要有g修饰符,返回一个迭代器,相当于多次全局执行exec的结果组成的数组
const reg = /(\d+)(\+)/g
const str = '44454+2+11'
console.log([...str.matchAll(reg)])
//
[
[
'44454+',
'44454',
'+',
index: 0,
input: '44454+2+11',
groups: undefined
],
[ '2+', '2', '+', index: 6, input: '44454+2+11', groups: undefined ]
]
- const newStr = str.replace(regexp|substr, newSubstr|function) 分别表示
- regexp 一个正则,被匹配到的字符被换成新的字符串
- substr 第一次出现的子串会被替换
- newSubstr 被替换的子串,其中可以包含以下替换模式
- $'
- $& 插入匹配到的子串
- $` 插入当前匹配的子串左边的内容
- $' 插入当前匹配的子串右边的内容
- $n 表示第n个捕获组
- $ 分组名
- function 创建新子串的函数,如果是全局模式每匹配一次调用一次,参数分别为
- match 相当于$&
- p1,p2, ... 相当于$n
- offset index
- string input
- NamedCaptureGroup 命名捕获组
- const newStr = str.replaceAll(regexp|substr, newSubstr|function) 将所有匹配到的子字符替换,一定要用g
- str.split([separator[, limit]]) 按照separator将字符串分组,separator本身会被移除,分别表示
- separator 分隔的位置,可以是字符串或者正则
- limit 返回数组最大长度
本部分已经结束了,接下来去ES2021特性全集(二)阅读剩下的内容吧!