0%

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

typeof无法精准的判断一个变量的类型,比如typeof null返回的是object,而typeof []返回的是object,这就需要我们使用其他方法来判断。

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

在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:

JavaScript identifiers

标识符命名规则

JavaScript标识符可以使用如下字符:

  • Unicode字母(包括中文)
  • 字母(A-Z,a-z)
  • 数字(0-9)
  • 下划线(_)- 通常用于变量的前缀或者后缀,用来表示私有变量
  • 美元符号($)- 通常用于一些library,比如jQuery。

注意事项

  • 标识符不能以数字开头
  • 标识符不能包含空格
  • 标识符不能是保留字

JavaScript 保留字

JavaScript 保留字是一些特殊的关键字,它们有特殊的用途,不能用作标识符。下面是一些JavaScript保留字:

1
2
3
4
break case catch class const continue debugger default delete do
else enum export extends false finally for function if import in instanceof
new null return super switch this throw true try typeof var void while with
yield

在strict mode下,还有一些额外的保留字:

1
implements interface let package private protected public static

冷知识

  • undefined/NaN/Infinity等等都属于全局变量,而不是一个关键字,所以你可以使用他们作为变量名,但是不建议这样做。比如下面的代码在Node环境是合法的。

    1
    2
    let undefined = 'Hello';
    console.log(undefined); // Hello
  • 可以使用中文作为变量名,但是不建议这样做。比如下面代码是合法的

    1
    2
    const 你好 = 'Hello';
    console.log(你好); // Hello

Cannot use import statement outside a module

产生这个错误的原因是因为你使用了import或者export语句,但是没有指定模块类型。在JavaScript中,有两种常用的模块类型:CommonJS和ES6 Modules。CommonJS是Node.js的模块化规范,它使用require来引入模块,使用module.exports来暴露接口。ES6 Modules是ES6的模块化规范,它使用import来引入模块,使用export来暴露接口。

解决办法

浏览器端

如果是浏览器端,需要将<script>标签的type属性设置为module,这样浏览器会将这个脚本当作ES6 Modules来处理。此时无论js文件的扩展名是.js还是.mjs都可以正常工作。

1
2
3
<script type="module">
import {add} from './math-utils.js';
</script>
1
2
3
4
// math-utils.js
export function add(a, b) {
return a + b;
}

Node.js端

方法一: 将文件的后缀名改为.mjs,这样Node.js会将这个文件当作ES6 Modules来处理。

1
2
3
4
// math-utils.mjs
export function add(a, b) {
return a + b;
}
1
2
// index.mjs
import {add} from './math-utils.mjs';

方法二: 在package.json中添加"type": "module"字段,这样Node.js会将所有的文件当作ES6 Modules来处理。

1
2
3
4
5
6
7
8
// package.json
{
"name": "json",
"version": "1.0.0",
"type": "module", // 添加这一行,就可以支持ESModule了。
"dependencies": {
}
}

总结

  • js - 常规js文件
  • mjs - ES6 Modules文件, 使用importexport来引入和暴露接口
  • cjs - CommonJS文件,使用requiremodule.exports来引入和暴露接口

Node.js环境可以使用任何上述文件格式,浏览器端其实不在意文件的后缀名,只要<script>标签的type属性设置为module,就可以使用ES6 Modules。

JavaScript Modules

Why use modules?

早期的JavaScript只是做一些简单的交互,所以不需要模块化。但是随着JavaScript的发展,现在的JavaScript已经可以做很多事情了,比如构建大型的应用程序,而Node.js的出现使得JavaScript甚至可以胜任某些Server端的工作,这时候模块化就显得非常重要了。

模块化的好处有很多,比如:

  • 代码复用
  • 代码隔离
  • 代码维护
  • 依赖管理

JavaScript模块化主要经历了以下几个阶段:

  • IIFE
  • CommonJS
  • AMD
  • UMD
  • ES6 Modules

IIFE

IIFE(Immediately Invoked Function Expression)是一种立即执行的函数表达式,它可以用来模拟模块化。IIFE的特点是:

  • 使用闭包来隔离作用域
  • 通过返回一个对象来暴露接口
1
2
3
4
5
6
const module = (function() {
...
return {
// expose some interface
};
})();

下面的代码使用IIFE封装了一个Person类,其中getName方法是public的,其他方法是private的。因为IIFE是定义后就立即执行的,所以这里我们使用一个person变量来接收IIFE的返回值,这样就可以调用getName方法了。

想要向外暴露任何接口,只需要在返回的对象中添加即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const person = (function person(firstName, lastName) {
const _firstName = firstName;
const _lastName = lastName;

function getFirstName() {
return _firstName;
}

function getLastName() {
return _lastName;
}

function getName() {
return `${getFirstName()} ${getLastName()}`;
}

return {
getName: getName,
};
})('Philip', 'Zhang');

console.log(person.getName()); // 'Philip Zhang'

CommonJS

CommonJS是Node.js的模块化规范,它的特点是:

  • 使用require来引入模块
  • 使用module.exports来暴露接口
1
2
// index.js
const module = require('./module');
1
2
3
4
// module.js
module.exports = {
// expose some interface
};

AMD

AMD(Asynchronous Module Definition)是另一种模块化规范,它的特点是:

  • 使用define来定义模块
  • 使用require来引入模块
1
2
3
4
5
6
define(['module'], function(module) {
...
return {
// expose some interface
};
});

UMD

UMD(Universal Module Definition)是一种通用的模块化规范,它的特点是:

  • 支持CommonJS和AMD
  • 通过判断typeof module来判断当前环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['module'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('module'));
} else {
// Browser
root.module = factory(root.module);
}
}(this, function (module) {
...
return {
// expose some interface
};
}));

ES6 Modules

ES6 Modules是JavaScript的模块化规范,它的特点是:

  • 使用import来引入模块
  • 使用export来暴露接口
1
2
// index.js
import module from './module';
1
2
3
4
// module.js
export default {
// expose some interface
};

Node.js中的ES6 Modules

Node.js从v13.2.0开始支持ES6 Modules,但是需要使用.mjs后缀名。

1
2
// index.mjs
import module from './module.mjs';
1
2
3
4
// module.mjs
export default {
// expose some interface
};

总结

JavaScript模块化的发展经历了很多阶段,从IIFE到CommonJS、AMD、UMD,最后到ES6 Modules。ES6 Modules是JavaScript的官方模块化规范,它的特点是简洁、易用,所以在现代的JavaScript开发中,推荐使用ES6 Modules。

References:

如何运行 JavaScript

运行JavaScript代码有很多方式,以下总结四种方式:

  • 使用html运行
  • 使用浏览器控制台运行
  • 使用Node.js REPL(Read-Eval-Print Loop)交互式运行
  • 使用Node.js运行JavaScript文件
  • 使用编辑器运行

使用html运行

在html文件中,使用<script>标签引入JavaScript文件,然后在浏览器中打开html文件即可运行JavaScript代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Run JavaScript</title>
</head>
<body>
<script type="text/javascript">
console.log('Hello, world!');
</script>
</body>
</html>

使用浏览器控制台运行(Chrome)

在浏览器中打开网页,然后按F12(Windows)或者Ctrl+Shift+I(Mac)打开控制台,输入JavaScript代码即可运行。

使用Node.js REPL交互式运行

在终端中输入node命令,进入Node.js REPL环境,然后输入JavaScript代码即可运行。

1
2
3
4
$ node
> console.log('Hello, world!');
Hello, world!
undefined

使用Node.js运行JavaScript文件

在终端中输入node命令,后面跟上JavaScript文件路径,即可运行JavaScript文件。

1
2
$ node index.js
Hello, world!

使用编辑器运行

  • VS Code可以使用Code Runner插件运行JavaScript文件,安装插件后,右键点击文件,选择Run Code即可运行。
  • WebStorm可以直接右键点击文件,选择Run即可运行。

今天有幸了解到了另外一个IED,Komodo,主要是针对Python和Perl的,但是也支持JavaScript,有空下载来试试。