0%

JavaScript Return

今天一起了解一下 JavaScript 中的 return 语句。

语法

return 语句用于指定函数的返回值。当 JavaScript 遇到 return 语句时,函数将立即返回,并且不再执行任何后续的语句。

1
return [expression]; // expression 是要返回的值
1
2
3
4
5
function add(a, b) {
return a + b;
}

console.log(add(1, 2)); // 3

如果省略 expression,或者 return 语句没有在函数中,则返回值为 undefined

1
return; // undefined

return后面省略表达式,返回值为undefined

1
2
3
4
5
6
7
function foo() {
const a = 1;
const b = 2;
return;
}

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

没有return语句,也返回undefined

1
2
3
4
5
6
function bar() {
const a = 1;
const b = 2;
}

console.log(bar()); // undefined

返回对象

return 语句返回复杂值时,return不能独占一行,否则会报错。

下面这样写会导致语法错误。

1
2
3
4
5
6
7
function getPerson() {
return // return 不能独占一行
{
name: 'zdd',
age: 18
};
}

正确的写法是:

1
2
3
4
5
6
function getPerson() {
return { // '{' 与 'return' 在同一行
name: 'zdd',
age: 18
};
}

try…catch…finally

如果在 try-catch-finally 语句块中使用了 return 语句,则执行顺序如下:

  1. 先计算return后面的表达式的值。
  2. 然后执行 finally 语句块中的代码。
  3. 最后return步骤1中计算出的值。
1
2
3
4
5
6
7
8
9
function foo() {
try {
console.log(1);
return 2;
} finally {
console.log('3');
}
}
console.log(foo()); // 1 3 2

再看一个复杂的例子

1
2
3
4
5
6
7
8
9
10
11
function foo() {
let a = 1;
try {
console.log(a);
return a++;
} finally {
console.log(a);
}
}

console.log(foo()); // 1 2 1

为啥结果是1 2 1呢?

  • try中`console.log(a)输出1,这个很好理解。
  • return a++,这里的a++是先返回a的值,然后再执行a++,所以return返回的是1
  • finallyconsole.log(a)输出的是2,因为a++执行了一次。

所以执行的顺序是:

  1. 先计算return后面的表达式,a = 2.
  2. 然后执行finally中的代码,输出a = 2
  3. 最后return返回1.

JavaScript Regex Groups

分组匹配是正则表达式中非常重要的一部分,它允许我们将多个字符组合在一起,并对它们进行操作。在JavaScript正则表达式中,我们可以使用括号 () 来创建一个分组。在这篇文章中,我们将学习如何使用分组匹配。

匿名分组

匿名分组的格式如下,其中 xxx 是要匹配的内容。匿名分组返回的匹配没有名字,只能通过数组下标的形式访问。

1
(xxx)

给定字符串Personal info: name: zdd, age: 18, gender: male,如果要从中提取出姓名,年龄和性别,则可以使用匿名分组来实现。

1
2
3
4
5
6
7
const str = `Personal info: name: zdd, age: 18, gender: male`;
const regex = /name: (\w+), age: (\d+), gender: (\w+)/;
const match = str.match(regex);
console.log(match); // 返回整个匹配 name: zdd, age: 18, gender: male
console.log(match[1]); // 第一个分组:zdd
console.log(match[2]); // 第二个分组:18
console.log(match[3]); // 第三个分组:male

在上面的例子中,正则表达式/name: (\w+), age: (\d+), gender: (\w+)/中有三个(),所以一共包含三个分组:

  • 第一个分组: \(w+) - 匹配zdd
  • 第二个分组:(\d+) - 匹配18
  • 第三个分组:(\w+) - 匹配male

使用匿名分组时,返回值是一个数组,数组第一个元素是整个匹配,我们要的分组匹配结果从数组第二个元素(下标1)开始,返回值如下:

1
2
3
4
5
6
7
8
9
[
'name: zdd, age: 18, gender: male',
'zdd', // 第一组
'18', // 第二组
'male', // 第三组
index: 0,
input: 'name: zdd, age: 18, gender: male',
groups: undefined
]

注意,使用匿名分组时,返回值中groups属性值为undefined,这个值只有使用命名匹配时才有值。

命名分组

命名分组的格式如下,其中 name 是分组的名称,xxx 是要匹配的内容。命名匹配返回的分组有名字,除了通过数组下标访问外,还可以通过对象属性的方式访问。

1
(?<name>xxx)

还是以上面的字符串为例,我们使用命名分组来提取姓名,年龄和性别。

1
2
3
4
5
6
7
const str = `Personal info: name: zdd, age: 18, gender: male`;
const regex = /name: (?<name>\w+), age: (?<age>\d+), gender: (?<gender>\w+)/;
const match = str.match(regex);
console.log(match.groups); // [Object: null prototype] { name: 'zdd', age: '18', gender: 'male' }
console.log(match.groups.name); // zdd
console.log(match.groups.age); // 18
console.log(match.groups.gender); // male

match返回值如下:与匿名匹配相比,命名匹配的返回值中groups属性不再是undefined,而是一个对象,对象的属性名就是分组的名称,属性值就是匹配的结果。

1
2
3
4
5
6
7
8
9
[
'name: zdd, age: 18, gender: male',
'zdd',
'18',
'male',
index: 0,
input: 'name: zdd, age: 18, gender: male',
groups: [Object: null prototype] { name: 'zdd', age: '18', gender: 'male' }
]

命名匹配的好处是,除了可以使用数组下标来访问分组匹配外,还可以使用groups字段来获取分组匹配。比如上面的match.groups.name就表示name分组对应的值。

再强调一次:groups字段只有在命名匹配时才有值,匿名匹配时用不到这个字段。

javascript regex summary

js中正则表达式的几种应用场景:

验证

  • RegExp.prototype.test

提取

  • String.prototype.match
  • String.prototype.matchAll
  • RegExp.prototype.exec
  1. match/exec方法,如果没有分组匹配,则通常取matches[0],如果有分组匹配,则取matches[1]、matches[2]...

替换

  • String.prototype.replace
  • String.prototype.replaceAll

拆分

  • String.prototype.split

javascript regex exec

exec是正则表达式的方法,用来返回匹配的字符串。

exec的返回值

exec的返回值是一个数组,数组的第一个元素是匹配的字符串,后面的元素是分组匹配的值。看一个列子:

1
2
3
4
const str = 'There are 3 dogs, 5 cats, 2 birds and 1 cow';
const regex = /(\d+) (cat|dog|bird|cow)/g;
const match = regex.exec(str);
console.log(match);

返回值如下:

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

循环处理

exec通常配合while循环使用,一边遍历,一边处理结果。注意,如果使用while循环处理,需要正则表达式中添加/g标志。

1
2
3
4
5
6
7
8
9
const str = 'There are 3 dogs, 5 cats, 2 birds and 1 cow';
const regex = /(\d+) (cat|dog|bird|cow)/g; // 添加/g标志
let match;
const result = {};
while ((match = regex.exec(str)) !== null) {
result[match[2]] = match[1];
}

console.log(result);

输出结果:

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

/g标志

如果想要使用while循环来遍历所有匹配的字符串,需要使用/g标志。否则会导致while死循环。

match/matchAll vs exec

  1. match/matchAll - 一次性返回所有匹配的字符串。
  2. exec - 逐个返回匹配的字符串。

match/matchAll使用/g时,需要对结果进行遍历。
exec是一边遍历,一边处理结果。

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