0%

javascript正则表达式之match

先说一个违反直觉的事情,虽然match是js正则表达式操作中非常重要的一个方法,但是它却不是regex对象的方法,而是字符串对象的方法。

match用來返回符合某个正则表达式的字符串,如果没有找到匹配的字符串则返回null

先看一个例子,下面的代码用来返回字符串str中的第一个数字字符。

1
2
3
const str = "There are 3 dogs, 5 cats, 2 birds and 1 cow";
const matches = str.match(/\d/);
console.log(matches[0]); // 3, 返回第一个匹配。

match的返回值

match的返回值比较特殊,分以下几种情况,

  • 如果没有找到匹配,返回null
  • 如果找到了匹配,返回一个数组。

返回数组时,又分为以下几种情况

  1. 如果正则表达式没有g标志(且未分组),返回一个数组,看如下代码。
1
2
3
const str = "There are 3 dogs, 5 cats, 2 birds and 1 cow";
const matches = str.match(/\d/);
console.log(matches);

返回值如下:

1
2
3
4
5
6
[
'3', // 匹配
index: 10, // 匹配值对应的下标
input: 'There are 3 dogs, 5 cats, 2 birds and 1 cow', // 原始字符串
groups: undefined // 分组匹配的值
]

此时,你需要的匹配值是matches[0]

  1. 如果正则表达式没有g标志且分组,(注意(\d)中的小括弧表示分组),返回一个数组,比之上面的返回值,多一个分组对应的值。
1
2
3
const str = "There are 3 dogs, 5 cats, 2 birds and 1 cow";
const matches = str.match(/(\d) cats/);
console.log(matches);

返回值如下:

1
2
3
4
5
6
7
8
[
'5 cats', // 匹配的整个字符串
'5', // 第一个分组匹配的值
index: 18, // 匹配值对应的下标
input: 'There are 3 dogs, 5 cats, 2 birds and 1 cow', // 原始字符串
groups: undefined // 分组匹配的值
]

此时,你需要的匹配值是matches[1]。(既然分组了,就要用分组的值呀,否则还分组干嘛呢?)

  1. 如果正则表达式有g标志(无论是否分组),返回一个数组,数组中的元素是所有匹配的值。
1
2
3
const str = "There are 3 dogs, 5 cats, 2 birds and 1 cow";
const matches = str.match(/\d/g); // 或者 str.match(/(\d)/g);
console.log(matches);

返回值如下:

1
[ '3', '5', '2', '1' ]

matchAll

match方法使用/g标记时,只能返回匹配的字符串(上面第三点),而无法返回分组信息,如果要返回分组信息,那么可以使用matchAll方法。

matchAll方法返回一个迭代器,而不是数组,所以不能直接输出,可以使用for...of循环遍历匹配的所有结果。

1
2
3
4
5
const str = "There are 3 dogs, 5 cats, 2 birds and 1 cow";
const matches = str.matchAll(/(\d) (cats|dogs)/g);
for (const match of matches) {
console.log(match);
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
'3 dogs', // 匹配的整个字符串
'3', // 第一个分组匹配的值
'dogs', // 第二个分组匹配的值
index: 10, // 匹配值对应的下标
input: 'There are 3 dogs, 5 cats, 2 birds and 1 cow', // 原始字符串
groups: undefined // 分组匹配的值
]
[
'5 cats', // 匹配的整个字符串
'5', // 第一个分组匹配的值
'cats', // 第二个分组匹配的值
index: 18, // 匹配值对应的下标
input: 'There are 3 dogs, 5 cats, 2 birds and 1 cow', // 原始字符串
groups: undefined // 分组匹配的值
]

假设有如下需求,将There are 3 dogs, 5 cats, 2 birds and 1 cow这句话中每种动物及其数量提出出来,放到一个对象中,则可以使用matchAll方法。

1
2
3
4
5
6
7
const str = 'There are 3 dogs, 5 cats, 2 birds and 1 cow';
const matches = str.matchAll(/(\d+) (cat|dog|bird|cow)/g);
const result = {};
for (const match of matches) {
result[match[2]] = match[1];
}
console.log(result);

输出如下:

1
{ dog: '3', cat: '5', bird: '2', cow: '1' }

整个需求如果要使用match方法来做,还是比较复杂的。

总结

什么时候用test, 什么时候用match

  1. 如果你只是想判断字符串是否满足某个规则,用test
  2. 如果你想要得到匹配的子串,用match
  3. 简单的字符串包含操作可以直接使用indexOfincludes等方法。

javascript-regex-test

简介

javascript对正则表达式的支持非常好,你可以通过多种方式在js中使用正则表达式,常用的有test, exec, match等方法。本文主要介绍test方法。test是javascript中Regex对象上的一个方法,用来检测字符串是否满足某种规则,如果你想要检测字符串是否满足某个规则,而不需要提取匹配后的字符串,那么test是最好的选择。举个例子:判断一个字符串中是否包含单词hello

1
2
3
4
5
6
7
8
const regex = new RegExp('hello');
const str = 'hello, world!';

if (regex.test(str)) {
console.log(true);
} else {
console.log(false);
}

由于js中正则表达式可以用//包裹,所以我们可以简化一下上面的代码:

1
2
3
const regex = /hello/;
const str = 'hello, world!';
console.log(regex.test(str)); // true

再简化一下,甚至可以写成一行:

1
console.log(/hello/.test('hello, world!')); // true

规则:regex.test(str), 这个表达式中,调用者是正则表达式,参数是字符串,返回值是布尔值。

大小写无关的匹配

默认情况下,正则表达式是匹配时是区分大小写的,如果要忽略大小写进行匹配,可以在正则表达式后面加上/i标志。

1
console.log(/hello/i.test('Hello, world!')); // true

多行匹配

默认情况下,正则表达式是单行匹配的,如果要多行匹配,可以在正则表达式后面加上/m标志。

1
2
3
4
5
6
7
const str = `
Hi,
hello,
world.
`;

console.log(/^hello/.test(str)); // false

因为默认情况下,^只匹配字符串的开头,所以上面的代码返回false。如果要多行匹配,可以这样写:

1
2
3
4
5
6
const str = `
Hi,
hello,
world.
`;
console.log(/^hello/m.test(str)); // true

/m表示多行匹配,即使hello不在字符串的首行,也能匹配到。
注意,这里/m真正的含义如下:

  • 如果不加/m^$匹配整个字符串的开头和结尾。
  • 如果加了/m^$匹配每一行的开头和结尾。

以上面代码为例,如果不加/m,则^hello匹配整个字符串的开头,但是整个字符串的开头是Hi,,所以返回false。如果加了/m,则^hello匹配每一行的开头,而第二行的开头是hello,所以返回true

注意, 如果没有给test传递参数,那么test会尝试匹配字符串undefined

1
console.log(/undefined/.test()); // true

全局匹配

默认情况下,test只会匹配第一个满足条件的字符串,如果要全局匹配,可以在正则表达式后面加上/g标志。

注意:你永远不需要在test方法上加/g标志,因为test方法只返回一个布尔值,而不是匹配到的字符串。
使用/g标志会改变正则表达式的lastIndex属性,这个属性会影响到test方法的行为,导致每次test匹配都从lastIndex开始,而不是从字符串的开头开始。

下面的代码中,为何第二次调用test方法返回false呢?

1
2
3
4
5
6
7
const regex = /foo/g;

// lastIndex = 0;
console.log(regex.test('foo')); // true

// lastIndex = 3;
console.log(regex.test('foo')); // false

分析一下:第一次匹配开始之前,lastIndex = 0, 指向字符串开头,第一次匹配成功后,lastIndex = 3, 下一次匹配从则从下标3开始,但是字符串foo长度为3,后面已经没有字符了,所以导致第二次匹配失败!崩溃吗?

/g选项通常用于String.prototype.replace方法。

1
2
3
const str = 'foobarfoo';
console.log(str.replace(/foo/, '')); // barfoo, 只替换了第一个foo
console.log(str.replace(/foo/g, '')); // bar, 替换了所有的foo

javascript中还有一个String.prototype.replaceAll方法,看名字好像会替换所有满足条件的匹配,但是使用这个方法时,必须添加/g标志,否则会报错,真是脱裤子放屁呀!

1
2
3
const str = 'foobarfoo';
console.log(str.replaceAll(/foo/, '')); // TypeError: String.prototype.replaceAll called with a non-global RegExp argument
console.log(str.replaceAll(/foo/g, '')); // bar

References

Hoisting in JavaScript

JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables, classes, or imports to the top of their scope, prior to execution of the code.

Variable Hoisting

1
2
console.log(a); // undefined
var a = 1;

The above code is equivalent to:

1
2
3
var a;
console.log(a); // undefined
a = 1;

注意变量提示只提升到它所在的作用域的顶部,而不是全局作用域的顶部。

1
2
3
4
5
6
7
8
9
function outer() {
console.log(a); // ReferenceError: a is not defined
function inner() {
console.log(a); // undefined
var a = 1;
}
inner();
}
outer();

The above code is equivalent to:

1
2
3
4
5
6
7
8
9
10
11
function outer() {
console.log(a); // ReferenceError: a is not defined
function inner() {
var a; // a is hoisted to the top of its enclosing function `inner`.
console.log(a); // undefined
a = 1;
}

inner();
}
outer();

Function Hoisting

1
2
3
4
foo(); // hello
function foo() {
console.log('hello');
}

The above code is equivalent to:

1
2
3
4
function foo() {
console.log('foo');
}
console.log(foo); // [Function: foo]

注意函数提升和变量提升一样,只提升到它所在的作用域的顶部,而不是全局作用域的顶部。

1
2
3
4
5
6
7
8
9
10
inner(); // ReferenceError: inner is not defined

function outer() {
inner(); // 'Hello'

function inner() {
console.log('Hello');
}
}
outer();

Hoisting in ES6

ES6中的letconst不会被提升,所以在使用之前必须先声明。letconst会产生暂时性死区(Temporal Dead Zone)。

1
2
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;

关于这一点实际上是有争议的,有些观点认为,letconst是被提升的,只是在TDZ中,不能访问。看下面的代码:

1
2
3
4
5
const x = 1;
{
console.log(x); // ReferenceError: Cannot access 'x' before initialization
const x = 2;
}

如果我们把const x = 2;注释掉,那么代码就可以正常运行,此时x使用的是外层的x。这说明const x = 2;是被提升的(进而掩盖了外层的x),只是在TDZ中不能访问。

1
2
3
4
5
const x = 1;
{
console.log(x); // 1
// const x = 2;
}

Temporal dead zone (TDZ)

A variable declared with let, const, or class is said to be in a "temporal dead zone" (TDZ) from the start of the block until code execution reaches the place where the variable is declared and initialized.
下面的代码中,

1
2
3
4
5
6
7
8
9
10
11
12
// Temporal dead zone (TDZ), TDZ 从block开始的地方开始,到其定义的地方结束。
// 在TDZ中访问let定义的变量会产生ReferenceError。
// 而var定义的变量则不存在此问题,因为var有hoisting(变量提升)
{
// TDZ starts at beginning of scope
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2; // End of TDZ (for foo)
const xxx = 3;
}

Arrow function is not hoisted.

1
2
3
4
foo(); // ReferenceError: Cannot access 'foo' before initialization
const foo = () => {
console.log('Hello');
}

Function expression is not hoisted.

1
2
3
4
foo(); // ReferenceError: Cannot access 'foo' before initialization
const foo = function() {
console.log('foo');
}

difference between variable hoisting and function hoisting

变量提升只提升变量的声明,不提升赋值,所以变量提升后的值是 undefined。

1
2
console.log(a); // undefined
var a = 1;

函数提升是整体提升,所以可以放心的调用。

1
2
3
4
foo(); // 'hello'
function foo() {
console.log('hello');
}

在Modern JavaScript中,应避免使用变量提升和函数提升,遵循以下三点可以保证:

  1. Use let and const instead of var
  2. Use strict mode.
  3. Always declare variables and functions before using them.

面试题

第一题

以下代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
function bar() {
return 3;
}

return bar();

function bar() {
return 8;
}
}

console.log(foo()); // 8

答案:输出8。

提升后的代码如下,注意:JavaScript中允许同名函数存在,后面的函数会覆盖前面的函数。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
function bar() {
return 3;
}

function bar() {
return 8;
}

return bar();
}

第二题

以下代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
var a = 1;

function b() {
a = 10;
return;
function a() {}
}

b();
console.log(a);
}

foo();

答案:1

提升后的代码如下,function a()的声明提升至function b()的顶部,而a = 10;是对function a()的再赋值,不会影响外部的变量a

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a;
a = 1;

function b() {
function a() {}
a = 10;
return;
}

b();
console.log(a);
}

如果我们把function a() {}注释掉,那么代码就会输出10,因为此时a = 10;是对外部变量a的赋值。

第三题

以下代码输出什么?

1
2
3
4
5
```

### 第四题
以下代码输出什么?
```javascript

Reference:

MDN: Hoisting

TCP Open and Close

TPC连接需要三次握手

第一次握手(客户端发送 SYN 报文给服务器,服务器接收该报文):客户端什么都不能确认;服务器确认了对方发送正常,自己接收正常。

第二次握手(服务器响应 SYN 报文给客户端,客户端接收该报文):客户端确认了:自己发送、接收正常,对方发送、接收正常;服务器确认了:对方发送正常,自己接收正常。

第三次握手(客户端发送 ACK 报文给服务器):客户端确认了:自己发送、接收正常,对方发送、接收正常; 服务器确认了:自己发送、接收正常,对方发送、接收正常

tcp-open

TCP断开需要四次挥手

四次挥手过程如下图所示,流程如下:

  1. 客户端数据数据发送完毕,想关闭连接,发送FIN给服务器端。此时客户端进入FIN-WAIT-1状态

  2. 服务器端收到客户端的FIN,并返回ACK给客户端,确认已经收到客户端的关闭通知。

    当客户端收到服务器端的ACK通知后,不能立即关闭连接,因为服务器端可能还要继续发送数据。所以此时客户端进入FIN-WAIT-2状态。

    也就是在这个时间点上,客户端只是不再发送数据了,但是可能还要接收数据。

  3. 待服务器端数据发送完毕,发送FIN给客户端,告知客户端,我也发完了,准备关闭。此时服务器端进入CLOSE-WAIT状态。

  4. 客户端收到来自服务端的FIN后,返回ACK给服务端,表示我收到你的关闭通知了,你可以关闭了,服务端收到客户端的ACK后,立即关闭,状态变为CLOSE。客户端发送完ACK后,进入TIME-WAIT状态,且等待2ms后自动关闭,状态变为CLOSE。

所以,为啥要等2ms才能关闭?

tcp-close

JavaScript Event Loop

Event loop是JavaScript中一个非常重要的知识点,也是面试中常考的内容,今天我们来详细了解一下。其实Event loop的过程非常简单,它是一个无限循环,Javascript引擎等待任务,执行任务,sleep,等待下一个任务,继续执行任务,如此循环往复。虽说简单,但是也有如下特点:

  1. 浏览器渲染只有在JS执行完才进行,这二者是互斥的。
  2. 所以如果一个任务执行时间过长,会导致页面卡顿,也会导致用户交互不流畅,比如点击按钮没反应。

比如下面这段代码:运行时你会发现,浏览器一直在执行count函数,而这函数需要运行很久,在它执行完之前,浏览器不会渲染页面,用户也无法进行交互。也就是你看不到progress进度,也看不到按钮,更别说点击这个按钮了。这就印证了上面第一点,执行JS和渲染页面是互斥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="progress"></div>
<button onclick='alert("Hello, world!")'>show alert</button>
<script>
function count() {
for (let i = 0; i < 1e7; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
</body>

那么问题来了,如何修复上面这个问题呢?答案是将1e7这么多的数字分批处理,比如每次处理1000个数字,然后让浏览器渲染一次,这样就可以看到进度条了,也可以点击按钮了。

那么如何分批呢?其实分批不是目的,目的是给浏览器一个渲染的机会,这时我们就可以使用setTimeout来实现,因为setTimeout中的回调函数是异步执行的,在执行完同步代码后,浏览器会渲染一次,然后再执行setTimeout中的回调函数。如此往复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<div id="progress"></div>
<button onclick='alert("Hello, world!")'>show alert</button>
<script>
let i = 0;
function count() {
// do a piece of the heavy job (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);

if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>

上面代码的执行顺序是这样的:

  1. 执行同步代码,先处理1000个数字。
  2. i < 1e7, 表示还有数字需要处理,所以调用setTimeout,将count函数放到宏任务队列中。
  3. 同步代码执行完毕,浏览器渲染一次,用户可以看到进度条。
  4. 从宏任务队列中取出count函数,继续处理1000个数字。
  5. 重复2-4步骤,直到i >= 1e7。

如此这般,既不影响浏览器更新dom,也不影响渲染,也不耽误用户交互。岂不美哉?

浏览器如何执行一段JavaScript代码?

当浏览器加载一个页面时,它会创建一个主线程来执行JavaScript代码。主线程是单线程的,意味着它一次只能执行一个任务。当主线程执行JavaScript代码时,它会遵循以下步骤:

  1. 先执行同步代码。
  2. 遇到异步代码时,做如下处理
    1. 如果是宏任务代码,将其回调函数放到宏任务队列中。
    2. 如果是微任务代码,将其回调函数放到微任务队列中。
  3. 同步代码执行完毕后,开始依次执行微任务队列中的微任务(即回调函数)。
  4. 浏览器渲染
  5. 微任务队列清空后,从宏任务队列取出一个任务(即回调函数)执行,然后回到步骤1

注意:步骤3,5的执行也遵循第1, 2两点规则。这么做是为了处理微任务和宏任务的嵌套关系。微任务队列执行每次都会清空微任务队列,而宏任务队列每次只执行一个任务。

什么异步代码?

异步代码是指不会立即执行的代码,而是在将来某个时间点执行的代码。常见的异步代码有:

  1. setTimeoutsetInterval
  2. Promiseasync/await
  3. MutationObserver等等。

除了异步代码,剩下的就是同步代码。

宏任务

  • setTimeout
  • setInterval

微任务

  • Promiseasync/await
  • queueMicrotask
  • MutationObserver
  • process.nextTick(Node.js)

对于Promise需要特别注意:Promise本身是同步代码,但是其.then, .catch, .finally对应的回调函数是异步代码。对于queueMicrotask,它是一个新的API,用于将一个微任务添加到微任务队列中。(If we’d like to execute a function asynchronously (after the current code), but before changes are rendered or new events handled, we can schedule it with queueMicrotask.)

面试题

1. 以下代码输出什么?

1
2
3
4
5
6
7
8
9
10
console.log('Start');

new Promise((resolve, reject) => {
console.log('resolve');
resolve(1);
}).then((res) => {
console.log(res);
});

console.log('End');

输出结果是:

1
2
3
4
Start
resolve
End
1

这个题考察的点就是Promise的构造函数体是同步代码,所以输出Start后紧接着会先输出resolve,然后是End。而.then方法中的回调函数是异步代码,所以会在同步代码执行完毕后执行,所以最后输出1.

1. 以下代码输出什么?

1
2
3
4
document.body.style = 'background:blue';
Promise.resolve().then(() => {
document.body.style = 'background:black';
});

答:用户会直接看到一个黑色的页面,蓝色页面根本不会出现,连闪现的过程也没有。根据Event Loop模型,代码执行过程如下:

  1. 执行同步代码,document.body.style = 'background:blue';,将页面背景色设置为蓝色。
  2. 遇到微任务Promise.resolve().then(),将其回调函数放到微任务队列中。
  3. 同步代码执行完毕,开始执行微任务队列中的微任务,即将页面背景色设置为黑色。
  4. 浏览器开始渲染页面,用户看到的是黑色的页面。

也就是说,在页面渲染之前,document.body.style早已经在 Promise 的回调函数中被赋值为black 了。
注意,浏览器会在每次微任务队列清空时渲染一次,如下图所示。
event-loop

以下代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(1);

setTimeout(() => console.log(2));

Promise.resolve().then(() => console.log(3));

Promise.resolve().then(() => setTimeout(() => console.log(4)));

Promise.resolve().then(() => console.log(5));

setTimeout(() => console.log(6));

console.log(7);

答案:1 7 3 5 2 6 4.
首先执行同步代码,输出如下:

  • 控制台:1 7
  • 微任务队列:[() => console.log(3), () => setTimeout(() => console.log(4)), () => console.log(5)]
  • 宏任务队列:[()=>console.log(2), ()=>console.log(6)]

然后执行微任务队列中的微任务,输出如下:

  • 控制台:1 7 3 5
  • 微任务队列:[] // 微任务队列清空
  • 宏任务队列:[()=>console.log(2), ()=>console.log(6), () => console.log(4)]

最后执行宏任务队列中的宏任务,输出如下:

  • 控制台:1 7 3 5 2 6 4
  • 微任务队列:[] // 微任务队列清空
  • 宏任务队列:[] // 宏任务队列清空

2. 以下代码输出什么?

注意以下代码需要在Node.js环境下运行。因为process.nextTick是Node.js的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log('Start');

setTimeout(() => {
console.log('Timeout');
}, 0);

Promise.resolve().then(() => {
console.log('Promise then');
});

async function asyncFunction() {
await Promise.resolve();
console.log('Async/Await');
}

asyncFunction();

// Node.js
process.nextTick(() => {
console.log('Next tick');
});

console.log('End');

答案:

1
2
3
4
5
6
Start
End
Promise then
Async/Await
Next tick
Timeout

还是按照以下执行顺序来分析:

  1. 先执行同步代码,输出StartEnd

  2. 在执行微任务队列代码,输出Promise thenAsync/AwaitNext tick

  3. 最后执行宏任务队列代码,输出Timeout。注意每次只执行一个宏任务,然后再执行微任务队列中的微任务,如此往复。

  4. 微任务队列每次执行都清空。

  5. 宏任务队列每次只取一个任务执行。

Reference:

Useful Online Tools

Web性能测试

  1. https://www.webpagetest.org/
  2. https://pagespeed.web.dev/

Online books

  1. You don’t know JS
  2. ES6 入门
  3. 深入理解JS特性
  4. Exploring JS
  5. Modern JavaScript: 中文版, 英文版

ASCII tree generator

https://ascii-tree-generator.com/

Regex

  • Regex 101 - Online regex tester and debugger, 最好用的正则表达式工具,没有之一。

JSON

  • Free Json Server - 一个免费的 JSON Server,可以用来测试前端请求。
  • Json Formatter - JSON 格式化工具,可以将 JSON 格式化成易读的格式。
  • Json schema generator - JSON Schema 生成工具,可以根据 JSON 生成 JSON Schema。

TypeScript

JavaScript

  • AST - Abstract Syntax Tree,可以将 JavaScript 代码转换成 AST 树,方便理解代码的结构。

UI Framework

Standard:

RFC - Request for Comments

Introduction

Promise就像它的名字一样,是指在未来某个时间将会发生的事情。这个事情会生成一个结果,我们叫做Result。

Promise有三种状态:

  • Pending: 初始状态,既不是成功状态,也不是失败状态。
  • Fulfilled: 意味着操作成功完成。
  • Rejected: 意味着操作失败。

这里,FullfilledRejected又统称为Settled。也就是说,一个Promise只要执行完毕有就算是Settled了, 无论是成功还是失败。

Promise API

Promise.all

Promise.all接收一个promise数组,返回一个新的promise。这个新的promise会在所有promise都resolve之后resolve,或者在任何一个promise reject之后reject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 1 resolved');
}, 1000);
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 2 resolved');
}, 2000);
});

Promise.all([promise1, promise2]).then((values) => {
console.log(values);
});

output:

1
['Promise 1 resolved', 'Promise 2 resolved']

假设Promise 2 reject了,那么Promise.all会reject,并且返回的promise的值是Promise 2 reject的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 1 resolved');
}, 1000);
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(Error('Promise 2 rejected'));
}, 2000);
});

Promise.all([promise1, promise2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});

output:

1
Error: Promise 2 rejected

注意,Promise.all大部分情况下是异步返回的,只有一种情况例外,那就是传递一个空数组。

1
Promise.all([]); // 立即同步返回 Promise { [] }

我们可以使用console.log来验证上述代码是同步返回的

1
2
3
4
console.log('start');
const p = Promise.all([]);
console.log(p);
console.log('end');

输出结果如下,可见Promise.all([])是同步返回的。

1
2
3
start
Promise { [] }
end

除了传递空数组外,其他所有情况都是异步返回,比如直接传递非promise数组。

1
const p = Promise.all([1, 2, 3]); // 异步返回。

思考题:
以下代码输会输出promise one running...吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('promise one running...');
resolve('one');
}, 1000);
});

const p2 = new Promise((resolve, reject) => {
reject('rejected');
});

Promise.all([p1, p2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});

解析:会输出’promise one running…’, 虽然Promise.all会在任何一个promise reject之后立即reject,但是剩下的promise还是会继续执行,直到resolve或者reject。注意这就是promise的不可取消性,如何要取消,请使用RxJS中的observable。

到这里我们需要总结一下Promise.all的返回值,一共有以下几种情况:

  1. 所有promise都resolve,返回一个数组,数组中包含了所有promise的resolve值。
  2. 任何一个promise reject,返回的promise会reject,其值是第一个reject的promise的值。
  3. 传递一个空数组,返回的promise会立即resolve,值是一个空数组。

Promise.allSettled

这个方法与Promise.all类似,不同的是,Promise.allSettled会等待所有promise都settled之后返回。settled的意思是promise已经resolve或者reject了。

Promise.allSettled的返回值是一个数组,数组中的每个元素都是一个对象,包含了promise的状态和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('one');
}, 1000);
});

const p2 = new Promise((resolve, reject) => {
reject('rejected');
});

// Using .catch:
Promise.allSettled([p1, p2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});

以上代码输出如下:

1
2
3
4
[
{ status: 'fulfilled', value: 'one' },
{ status: 'rejected', reason: 'rejected' }
]

Promise.any

输入的Promise数组中,任意一个promise resolve,返回的promise就resolve。如果所有的promise都reject,返回的promise就reject。

以下代码输出quick,因为p2最快resolve。

1
2
3
4
5
6
const p1 = Promise.reject(0);
const p2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'));
const p3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'));

const promises = [p1, p2, p3];
Promise.any(promises).then((value) => console.log(value));

当所有promise都reject时,Promise.any会返回``AggregateError`,这个错误包含了所有的reject值。

1
2
3
4
const p1 = Promise.reject(0);
const p2 = Promise.reject(1);
const promises = [p1, p2];
Promise.any(promises).then((value) => console.log(value));

如果给Promise.any传递一个空数组,返回的promise会reject,错误信息是[AggregateError: All promises were rejected] { [errors]: [] }.

Promise.race

这个API最简单了,看名字就知道了,谁先返回就是谁,不论是resolve还是reject。
Promise.race接收一个promise数组,返回一个新的promise。这个新的promise会在任意一个promise resolve或者reject之后resolve或者reject。

下面代码输出two,因为p2最快resolve。

1
2
3
4
5
6
7
8
9
10
11
const p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});

const p2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});

Promise.race([p1, p2]).then((value) => {
console.log(value);
});

注意:如果给Promise.race传递一个空数组,返回的promise会一直pending,因为没有promise可以resolve或者reject。这就好比一个没有人参加的百米赛跑,永远不会有人冲过终点。

Promise in setTimeout

setTimeout is macro-task, and promise is micro-task. So the promise will be executed before the setTimeout.

1
2
3
4
5
6
7
8
9
10
const promise1 = Promise.resolve('Promise 1 resolved');
const promise2 = Promise.resolve('Promise 2 resolved');

const promise = Promise.all([promise1, promise2]);
console.log(promise);

// Using setTimeout, we can execute code after the queue is empty
setTimeout(() => {
console.log(promise);
});

output:

1
2
Promise { <pending> }
Promise { [ 'Promise 1 resolved', 'Promise 2 resolved' ] }

Why the output?

  1. Promise.all is a micro-task, so it will be executed before the setTimeout macro-task.
  2. The promise is pending when we log it for the first time.
  3. After the Promise.all is executed, the promise is resolved
  4. The setTimeout is a macro-task, so it will be executed after the queue is empty(no micro-task in the queue).
  5. The promise is resolved when we log it for the second time

Introduction

Promise就像它的名字一样,是指在未来某个时间将会发生的事情。这个事情会生成一个结果,我们叫做Result。

Promise有三种状态:

  • Pending: 初始状态,既不是成功状态,也不是失败状态。
  • Fulfilled: 意味着操作成功完成。
  • Rejected: 意味着操作失败。

这里,FullfilledRejected又统称为Settled。也就是说,一个Promise只要执行完毕有就算是Settled了, 无论是成功还是失败。

Promise API

Promise.all

Promise.all接收一个promise数组,返回一个新的promise。这个新的promise会在所有promise都resolve之后resolve,或者在任何一个promise reject之后reject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 1 resolved');
}, 1000);
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 2 resolved');
}, 2000);
});

Promise.all([promise1, promise2]).then((values) => {
console.log(values);
});
``
output:
```javascript
['Promise 1 resolved', 'Promise 2 resolved']

假设Promise 2 reject了,那么Promise.all会reject,并且返回的promise的值是Promise 2 reject的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 1 resolved');
}, 1000);
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(Error('Promise 2 rejected'));
}, 2000);
});

Promise.all([promise1, promise2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});

output:

1
Error: Promise 2 rejected

注意,Promise.all大部分情况下是异步返回的,只有一种情况例外,那就是传递一个空数组。

1
Promise.all([]); // 立即同步返回 Promise { [] }

我们可以使用console.log来验证上述代码是同步返回的

1
2
3
4
console.log('start');
const p = Promise.all([]);
console.log(p);
console.log('end');

输出结果如下,可见Promise.all([])是同步返回的。

1
2
3
start
Promise { [] }
end

除了传递空数组外,其他所有情况都是异步返回,比如直接传递非promise数组。

1
const p = Promise.all([1, 2, 3]); // 异步返回。

思考题:
以下代码输会输出promise one running...吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('promise one running...');
resolve('one');
}, 1000);
});

const p2 = new Promise((resolve, reject) => {
reject('rejected');
});

Promise.all([p1, p2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});

解析:会输出’promise one running…’, 虽然Promise.all会在任何一个promise reject之后立即reject,但是剩下的promise还是会继续执行,直到resolve或者reject。注意这就是promise的不可取消性,如何要取消,请使用RxJS中的observable。

到这里我们需要总结一下Promise.all的返回值,一共有以下几种情况:

  1. 所有promise都resolve,返回一个数组,数组中包含了所有promise的resolve值。
  2. 任何一个promise reject,返回的promise会reject,其值是第一个reject的promise的值。
  3. 传递一个空数组,返回的promise会立即resolve,值是一个空数组。

Promise.allSettled

这个方法与Promise.all类似,不同的是,Promise.allSettled会等待所有promise都settled之后返回。settled的意思是promise已经resolve或者reject了。

Promise.allSettled的返回值是一个数组,数组中的每个元素都是一个对象,包含了promise的状态和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('one');
}, 1000);
});

const p2 = new Promise((resolve, reject) => {
reject('rejected');
});

// Using .catch:
Promise.allSettled([p1, p2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});

以上代码输出如下:

1
2
3
4
[
{ status: 'fulfilled', value: 'one' },
{ status: 'rejected', reason: 'rejected' }
]

Promise.any

输入的Promise数组中,任意一个promise resolve,返回的promise就resolve。如果所有的promise都reject,返回的promise就reject。

以下代码输出quick,因为p2最快resolve。

1
2
3
4
5
6
const p1 = Promise.reject(0);
const p2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'));
const p3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'));

const promises = [p1, p2, p3];
Promise.any(promises).then((value) => console.log(value));

当所有promise都reject时,Promise.any会返回``AggregateError`,这个错误包含了所有的reject值。

1
2
3
4
const p1 = Promise.reject(0);
const p2 = Promise.reject(1);
const promises = [p1, p2];
Promise.any(promises).then((value) => console.log(value));

如果给Promise.any传递一个空数组,返回的promise会reject,错误信息是[AggregateError: All promises were rejected] { [errors]: [] }.

Promise.race

这个API最简单了,看名字就知道了,谁先返回就是谁,不论是resolve还是reject。
Promise.race接收一个promise数组,返回一个新的promise。这个新的promise会在任意一个promise resolve或者reject之后resolve或者reject。

下面代码输出two,因为p2最快resolve。

1
2
3
4
5
6
7
8
9
10
11
const p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});

const p2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});

Promise.race([p1, p2]).then((value) => {
console.log(value);
});

注意:如果给Promise.race传递一个空数组,返回的promise会一直pending,因为没有promise可以resolve或者reject。这就好比一个没有人参加的百米赛跑,永远不会有人冲过终点。

Promise in setTimeout

setTimeout is macro-task, and promise is micro-task. So the promise will be executed before the setTimeout.

1
2
3
4
5
6
7
8
9
10
const promise1 = Promise.resolve('Promise 1 resolved');
const promise2 = Promise.resolve('Promise 2 resolved');

const promise = Promise.all([promise1, promise2]);
console.log(promise);

// Using setTimeout, we can execute code after the queue is empty
setTimeout(() => {
console.log(promise);
});

output:

1
2
Promise { <pending> }
Promise { [ 'Promise 1 resolved', 'Promise 2 resolved' ] }

Why the output?

  1. Promise.all is a micro-task, so it will be executed before the setTimeout macro-task.
  2. The promise is pending when we log it for the first time.
  3. After the Promise.all is executed, the promise is resolved
  4. The setTimeout is a macro-task, so it will be executed after the queue is empty(no micro-task in the queue).
  5. The promise is resolved when we log it for the second time

Debug JavaScript Applications

我平时调试的时候,基本都是以打log的方式实现的,也就是用console.log一顿输出,这种方式简单粗暴,但是确实比较费时,还是要掌握正统的调试方法。现在步骤记录如下。

  1. 启动项目
  2. 来到项目所在的网页,按下F12打开开发者工具。
  3. 找到source tab, 然后找到以webpack://开头的源码。
  4. 依次展开目录,找到自己的代码,单击代码行左边的行号,就可以添加断点,然后按下F5,刷新页面,这时如果触发到断点,就会停下来。

js-debug

如果你的项目比较新,使用的是ESBuild + Vite来构建的,那么源文件如下图所示:
js-debug-esbuild

Set breakpoint in code

如果觉得以上办法设置断点太麻烦,也可以在代码里面设置,找到对应的代码行数,在前面加一行叫做 debugger,待项目热更新完毕,页面就会停在断点处。十分的方便。

set breakpoint

是不是很方便呢?

JavaScript Data Types

JavaScript是一种弱类型语言,它有八种数据类型,分别是:

  • String
  • Number
  • BigInt
  • Boolean
  • Null
  • Undefined
  • Symbol
  • Object

除了Object之外,其他的数据类型都是原始类型(Primitive types)。
注意:JavaScript中的Array、Function、Set、Map、RegExp等等都是Object类型。

如何判断一个变量的类型

在JavaScript中,我们可以使用typeof操作符来判断一个变量的类型。typeof操作符返回一个字符串,表示变量的类型。

1
2
3
4
5
6
7
8
9
typeof 'Hello'; // string
typeof 123; // number
typeof true; // boolean
typeof null; // object
typeof undefined; // undefined
typeof Symbol('Hello'); // symbol
typeof {}; // object
typeof []; // object
typeof function(){}; // function

注意:虽然Function类型是Object的子类型,但是typeof操作符返回的是function

如何精准的判断一个变量的类型

在JavaScript中,我们可以使用Object.prototype.toString方法来精准的判断一个变量的类型。

1
2
3
4
5
Object.prototype.toString.call('Hello'); // [object String]
Object.prototype.toString.call(123); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call([]); // [object Array]

注意:Object.prototype.toString方法返回的是一个字符串,表示变量的类型。所以我们可以封装一个函数来判断一个变量的类型。(注意:判断数组可以用更好的方法 - Array.isArray方法)

1
2
3
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}

以下几个需要注意:

1
2
3
4
5
6
typeof typeof 1; // string, typeof 永远返回字符串。
typeof null; // object, 这是一个历史遗留问题。看这里:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null
typeof NaN; // number, NaN是一个特殊的number类型。
typeof class C {}; // function, class是一个语法糖,本质上还是function。
typeof something; // undefined, 如果变量没有声明,那么typeof返回undefined。不会报错。
typeof document.all; // undefined, All current browsers expose a non-standard host object document.all with type undefined

instanceof 操作符

instanceof操作符用来判断一个对象是否是某个构造函数的实例。

1
2
3
const arr = [1, 2, 3];
arr instanceof Array; // true
arr instanceof Object; // true

References: