let var const

var变量提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function testVar() {
var x = 1;
if (true) {
var x = 2;
console.log(x); // 输出什么?2
}
console.log(x); // 输出什么?2
}

function testLet() {
let y = 1;
if (true) {
let y = 2;
console.log(y); // 输出什么?2
}
console.log(y); // 输出什么?1
}

testVar();
testLet();

const定义数据的不可变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const z = 1;
try {
z = 2; // 会发生什么?
} catch (e) {
console.log(e.message); // 输出什么?
}

const obj = { a: 1 };
obj.a = 2; // 会发生什么?
console.log(obj.a); // 输出什么?2

try {
obj = { a: 3 }; // 会发生什么?
} catch (e) {
console.log(e.message); // 输出什么?
}

暂时死区

1
2
3
4
5
6
7
8
9
10
function temporalDeadZone() {
console.log(a); // 输出什么?error
let a = 1;
}

try {
temporalDeadZone();
} catch (e) {
console.log(e.message); // 输出什么?
}

块级作用域

1
2
3
4
5
6
{
let a = 1;
var b = 2;
}
console.log(typeof a); // 输出 "undefined"
console.log(typeof b); // 输出 "number"

循环的块级作用域

1
2
3
4
5
6
7
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000); // 输出什么?3 3 3
}

for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 1000); // 输出什么?0 1 2
}
  • var 声明的变量在循环中具有函数作用域,导致所有 setTimeout 回调函数共享同一个变量。
  • let 声明的变量在循环中具有块级作用域,每次迭代都会创建一个新的变量,因此每个 setTimeout 回调函数捕获的值是独立的。

函数作用域和块级作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function scopeTest() {
var x = 1;
let y = 2;
const z = 3;

{
var x = 4;
let y = 5;
const z = 6;
console.log(x); // 输出什么?4
console.log(y); // 输出什么?5
console.log(z); // 输出什么?6
}

console.log(x); // 输出什么?4
console.log(y); // 输出什么?2
console.log(z); // 输出什么?3
}

scopeTest();

异步和闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createFunctions() {
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(function() {
console.log(i);
});
}
return arr;
}

var funcs = createFunctions();
funcs[0](); // 输出什么?3
funcs[1](); // 输出什么?3
funcs[2](); // 输出什么?3

异步和promise

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

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

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

console.log('end');

作用域与闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
function outer() {
var x = 10;
function inner() {
console.log(x);//10
}
return inner;
}

var closure = outer();
closure(); // 输出什么?10

var x = 20;
closure(); // 输出什么?10

异步与 async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}

async function async2() {
console.log('async2');
}

console.log('script start');

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

async1();

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

console.log('script end');

this

1
2
3
4
5
6
7
8
9
10
11
const obj = {
value: 42,
printValue: function() {
console.log(this.value);
}
};

const print = obj.printValue;

obj.printValue(); // 输出什么?
print(); // 输出什么?

原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name) {
this.name = name;
}

Person.prototype.greet = function() {
console.log('Hello, ' + this.name);
};

const alice = new Person('Alice');
const bob = new Person('Bob');

alice.greet(); // 输出什么?
bob.greet(); // 输出什么?

Person.prototype.greet = function() {
console.log('Hi, ' + this.name);
};

alice.greet(); // 输出什么?
bob.greet(); // 输出什么?

代码输出

第1题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const first = () =>
new Promise((resolve, reject) => {
console.log(3)
let p = new Promise((resolve, reject) => {
console.log(7)
setTimeout(() => {
console.log(5)
resolve(6)
}, 0)
resolve(1)
})
resolve(2)
p.then((arg) => {
console.log(arg)
})
})
first().then((arg) => {
console.log(arg)
})
console.log(4)

详细的执行流程

  1. 同步代码console.log(3)console.log(7) 立即执行并输出。
  2. 同步代码console.log(4)first 函数外部同步执行,输出 4
  3. 微任务
    • first 函数返回的 Promise 被解决,first().then(...) 中的回调执行,输出 2
    • p 的 Promise 被解决,p.then(...) 中的回调执行,输出 1
  4. 宏任务setTimeout 的回调在所有微任务完成后执行,输出 5

这样,输出顺序是:3 7 4 1 2 5

this指向问题

this:

基本概念:在函数调用的时候决定,不是在函数定义的时候决定

不同上下文的规则

箭头函数中的this

显示绑定:call,bind,apply的区别

事件循环

事件循环(Event Loop)是 JavaScript 的核心机制之一,它负责管理 JavaScript 执行时的任务调度,确保异步任务(如定时器、I/O 操作、用户事件等)能够在正确的时间执行。了解事件循环有助于理解 JavaScript 的非阻塞、单线程执行模型。

JavaScript 的单线程特性

JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。由于 JavaScript 通常用于操作 DOM(文档对象模型)和处理用户交互,如果多个任务同时修改界面或状态,将很难管理和维护。因此,JavaScript 的单线程特性确保了任务的有序执行。

同步任务与异步任务

  • 同步任务:这些任务会按顺序立即执行。它们会阻塞后续代码的执行,直到完成。
  • 异步任务:这些任务不会立即执行,而是注册后等待某个事件或条件满足时再执行,不会阻塞主线程。

事件循环的核心组成部分

事件循环由以下几个主要部分组成:

  1. 执行栈(Call Stack):同步任务会被放入执行栈中,按顺序执行。执行栈是一个 LIFO(后进先出)结构,任务会按入栈顺序执行完毕后出栈。

  2. 任务队列(Task Queue):异步任务的回调会被放入任务队列。任务队列可以进一步分为宏任务队列微任务队列

    • 宏任务队列(Macrotask Queue):包含大多数异步任务的回调,如 setTimeoutsetInterval、I/O 操作、setImmediate(Node.js 中)等。
    • 微任务队列(Microtask Queue):包含较小的异步任务回调,如 Promise.thencatchfinallyMutationObserverqueueMicrotask 等。
  3. 事件循环(Event Loop):事件循环的主要作用是监控执行栈和任务队列,按顺序调度任务。它的执行流程如下:

    • 当执行栈为空时,事件循环会从微任务队列开始,执行所有微任务,直到队列为空。
    • 之后,事件循环会从宏任务队列中取出第一个任务,放入执行栈并执行。
    • 执行完一个宏任务后,事件循环再次检查微任务队列并执行所有微任务。
    • 以上过程反复进行,确保所有任务都能在适当的时间执行。

事件循环的执行流程

  1. 执行栈处理同步任务:执行栈中的同步任务按照顺序立即执行。
  2. 执行微任务队列:一旦执行栈为空,事件循环会检查微任务队列并执行所有的微任务。
  3. 执行宏任务:微任务队列清空后,事件循环会从宏任务队列中取出第一个任务并执行。
  4. 重复检查和执行:每次执行完一个宏任务后,事件循环会再次检查微任务队列。如果微任务队列中有任务,所有微任务会在进入下一个宏任务前执行完毕。

code

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

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

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

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

console.log('End');

执行流程分析

  1. 执行同步代码

    • console.log('Start') 输出 Start
    • setTimeout(() => {...}, 0) 回调被注册,加入宏任务队列。
    • Promise.resolve().then(() => {...}) 回调被注册,加入微任务队列。
    • setTimeout(() => {...}, 0) 回调被注册,加入宏任务队列。
    • console.log('End') 输出 End
  2. 执行微任务

    • 微任务队列中 Promise.then 回调被执行,输出 Promise 1
  3. 执行宏任务

    • 宏任务队列中第一个 setTimeout 回调被执行,输出 Timeout 1
  4. 再次执行微任务(无微任务,直接跳过)。

  5. 继续执行宏任务

    • 宏任务队列中的第二个 setTimeout 回调被执行,输出 Timeout 2

最终输出顺序

1
2
3
4
5
Start
End
Promise 1
Timeout 1
Timeout 2

事件循环的优势与应用

  • 非阻塞 I/O:事件循环允许 JavaScript 处理 I/O 操作(如网络请求、文件读写)而不阻塞主线程,使得程序可以高效地处理并发任务。
  • 异步编程模型:通过事件循环,JavaScript 可以以同步代码的风格处理异步操作(如 async/await),提供更直观的代码结构。
  • 实时用户交互:事件循环确保浏览器在处理 JavaScript 代码时仍然能够响应用户交互,如点击、滚动等操作。

常见问题与误区

  • 阻塞主线程:长时间的同步任务会阻塞主线程,使得事件循环无法及时处理微任务和宏任务,导致页面卡顿。
  • 过度使用微任务:大量的微任务会阻塞宏任务的执行,导致用户交互延迟。合理控制微任务的数量和复杂度是性能优化的关键。

总结

  • 事件循环是 JavaScript 实现异步操作的核心机制。
  • 通过调度执行栈、微任务队列和宏任务队列,事件循环确保了任务的有序执行。
  • 微任务优先于宏任务执行,确保紧急任务(如 Promise 回调)能够快速响应。
  • 事件循环的设计让 JavaScript 能够在单线程环境下高效处理并发任务。

希望这个解释帮助你理解事件循环的工作机制!如果有任何问题或需要进一步的澄清,欢迎继续提问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
console.log('Script start');

async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
await async3();
console.log('after async3');
}

async function async2() {
console.log('async2');
}

async function async3() {
console.log('async3 starts');
return new Promise((resolve) => {
console.log('async3 promise');
resolve();
}).then(() => {
console.log('async3 promise then');
});
}

async1();

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

setTimeout(() => {
console.log('setTimeout 2');
Promise.resolve().then(() => {
console.log('setTimeout 2 promise');
});
}, 0);

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

new Promise((resolve) => {
console.log('Promise 2');
resolve();
}).then(() => {
console.log('Promise 2 then 1');
return Promise.resolve(); // 返回另一个 Promise
}).then(() => {
console.log('Promise 2 then 2');
});

console.log('Script end');

同步代码执行

最先执行的是同步代码,这些都是直接写在函数外部或函数内部没有被 await 关键字暂停的代码:

  • console.log('Script start')
  • console.log('async1 start')(在 async1() 的第一行)
  • console.log('async2')(在 async2() 的唯一一行,被 async1() 调用)
  • console.log('Promise 1')(在 new Promise 的构造函数中)
  • console.log('Promise 2')(在另一个 new Promise 的构造函数中)
  • console.log('Script end')

微任务队列的处理

微任务包括所有 Promise.then/catch/finally 回调和 async 函数中 await 后的代码。它们在每次事件循环的末尾执行,优先级高于宏任务(如 setTimeout):

  • console.log('async1 end') — 来自 async1()await async2() 之后的代码。
  • console.log('async3 starts')console.log('async3 promise') — 在 async3() 内部,被 async1()await async3() 调用。
  • console.log('Promise 1 then 1') — 来自 Promise 1 的第一个 .then 回调。
  • console.log('Promise 2 then 1') — 来自 Promise 2 的第一个 .then 回调。
  • console.log('async3 promise then')async3()Promise.then 回调。
  • console.log('Promise 1 then 2') — 来自 Promise 1 的第二个 .then 回调。
  • console.log('after async3')async3() 完成后的代码。
  • console.log('Promise 2 then 2') — 来自 Promise 2 的第二个 .then 回调。

宏任务队列的处理

宏任务如 setTimeoutsetInterval 在微任务之后,一个事件循环迭代的最后执行:

  • console.log('setTimeout 1')
  • console.log('setTimeout 2')
  • console.log('setTimeout 2 promise') — 在 setTimeout 2 中的 Promise.resolve().then()

解释

  • await 的工作:每次遇到 await,当前的 async 函数会暂停执行,直到等待的 Promise 被解决。解决后,函数的剩余部分(await 后的代码)将作为微任务排队。
  • 事件循环:JavaScript 运行时使用事件循环来管理同步任务、微任务和宏任务。在每个事件循环迭代中,首先执行同步代码,然后是所有排队的微任务,最后是一个宏任务(如果有多个,则每个迭代一个)。
  • 微任务优先于宏任务:这就是为什么所有的 Promise.then 回调和 async 函数的延续都在任何 setTimeout 回调之前执行。

这个执行顺序反映了 JavaScript 的非阻塞异步执行模型,通过事件循环以及微任务和宏任务的队列机制来实现。如果需要进一步的解释或有其他问题,请随时提出!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
console.log("Script start");

Promise.resolve()
.then(() => {
console.log("Promise 1 - then 1");
throw new Error("Error in Promise 1");
})
.catch((error) => {
console.log("Promise 1 - catch");
})
.finally(() => {
console.log("Promise 1 - finally");
});

Promise.resolve()
.then(() => {
console.log("Promise 2 - then 1");
})
.then(() => {
console.log("Promise 2 - then 2");
})
.finally(() => {
console.log("Promise 2 - finally");
});

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

console.log("Script end");

我们可以通过一个包含 Promisethencatchfinally 的例子,进一步详细说明事件循环中微任务的执行顺序。让我们先写一个包含这些结构的代码,并逐步分析它的执行顺序。

code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
console.log("Script start");

Promise.resolve()
.then(() => {
console.log("Promise 1 - then 1");
throw new Error("Error in Promise 1");
})
.catch((error) => {
console.log("Promise 1 - catch");
})
.finally(() => {
console.log("Promise 1 - finally");
});

Promise.resolve()
.then(() => {
console.log("Promise 2 - then 1");
})
.then(() => {
console.log("Promise 2 - then 2");
})
.finally(() => {
console.log("Promise 2 - finally");
});

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

console.log("Script end");

预期输出:

1
2
3
4
5
6
7
8
9
Script start
Script end
Promise 1 - then 1
Promise 2 - then 1
Promise 1 - catch
Promise 2 - then 2
Promise 1 - finally
Promise 2 - finally
setTimeout

事件循环的执行过程分析:

  1. 执行同步代码(同步任务)

    • console.log("Script start") 会被立即执行,输出 Script start
    • 两个 Promise.resolve() 代码块被解析,并且它们的 .then.catch.finally 回调被放入微任务队列,等待同步代码执行完毕后执行。
    • setTimeout 的回调被注册,它会被放入宏任务队列中。
    • console.log("Script end") 会被立即执行,输出 Script end

    这时,微任务队列包含:

    • Promise 1 的第一个 .then
    • Promise 2 的第一个 .then

    宏任务队列包含:

    • setTimeout 的回调
  2. 执行微任务队列(微任务1)

    • 微任务队列中的第一个任务是 Promise 1.then 回调。它被执行,输出 Promise 1 - then 1,并且在该 .then 回调中抛出了一个错误。
    • 由于错误被抛出,Promise 1 的链跳过了下一个 .then,直接进入 .catch 回调。.catch 回调被执行,输出 Promise 1 - catch
    • 最后,Promise 1.finally 回调被执行,输出 Promise 1 - finally
  3. 继续执行微任务队列(微任务2)

    • 接下来是 Promise 2 的第一个 .then 回调,它被执行,输出 Promise 2 - then 1
    • 然后,Promise 2 的第二个 .then 回调被执行,输出 Promise 2 - then 2
    • 最后,Promise 2.finally 回调被执行,输出 Promise 2 - finally

    此时,微任务队列已清空。

  4. 执行宏任务队列

    • 现在微任务队列已经清空,事件循环会去执行宏任务队列中的第一个宏任务——setTimeout 的回调。它被执行,输出 setTimeout

为什么是这样的执行顺序?

  • Promise 的 .then 回调在创建 Promise 时会被立即注册为微任务。一旦同步代码执行完毕,微任务队列中的 .then 回调会依次执行。
  • 如果 .then 回调中抛出了错误,.catch 会处理这个错误,并在相应的位置被调用。
  • .finally 回调无论 Promise 是 resolved 还是 rejected 都会执行,因此 Promise 1Promise 2.finally 都会在它们的 .then.catch 之后被调用。
  • 微任务队列优先级高于宏任务队列,因此在执行完所有的同步代码和微任务之后,才会去执行宏任务队列中的 setTimeout 回调。

事件循环的执行机制再总结:

  1. 执行所有同步代码(如 console.log("Script start")console.log("Script end"))。
  2. Promise.then.catch.finally 回调放入微任务队列。
  3. 微任务队列中的任务在同步代码执行完之后执行。每轮事件循环中,所有微任务(如 Promise 的回调)都必须执行完毕,才能开始执行下一个宏任务。
  4. 宏任务(如 setTimeout)在微任务完成后才执行。

更复杂的链式结构和错误处理

假设我们再增加一些复杂的链式结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log("Script start");

Promise.resolve()
.then(() => {
console.log("Promise 1 - then 1");
return Promise.reject("Error in Promise 1");
})
.then(() => {
console.log("Promise 1 - then 2");
})
.catch((error) => {
console.log("Promise 1 - catch:", error);
})
.finally(() => {
console.log("Promise 1 - finally");
});

console.log("Script end");

输出顺序:

1
2
3
4
5
Script start
Script end
Promise 1 - then 1
Promise 1 - catch: Error in Promise 1
Promise 1 - finally

在这个例子中,Promise 1 的第一个 .then 返回了一个被拒绝的 Promise,所以第二个 .then 被跳过,直接进入 .catch,处理错误后执行 .finally

总结

  • 微任务(Promise.then/catch/finally在同步任务结束后立即执行。
  • 错误处理链会跳过 .then 并直接进入 .catch,但 .finally 会总是被执行。
  • 微任务优先于宏任务,所有微任务执行完之后才会执行下一个宏任务(如 setTimeout)。

希望这个例子能够帮助你理解 Promisethencatchfinally 在事件循环中的执行顺序!如果有更多问题,欢迎继续提问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log("Script start");

Promise.resolve()
.then(() => {
console.log("Promise 1 - then 1");
return Promise.reject("Error in Promise 1");
})
.then(() => {
console.log("Promise 1 - then 2");
})
.catch((error) => {
console.log("Promise 1 - catch:", error);
})
.finally(() => {
console.log("Promise 1 - finally");
});

console.log("Script end");

输出结果

1
2
3
4
5
Script start
Script end
Promise 1 - then 1
Promise 1 - catch: Error in Promise 1
Promise 1 - finally

如果promise遇到error

当一个 Promise 链中的某个 .then() 方法抛出错误(或者返回一个被拒绝的 Promise),该错误会被跳过后续的 .then() 方法直到被一个 .catch() 方法捕获。如果在这个错误之后没有相应的 .catch() 方法来处理错误,那么这个错误会继续向下传递,直到找到一个 .catch(),或者错误完全没有被捕获,导致在控制台显示未捕获的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Promise.resolve()
.then(() => {
console.log("第一个 then 正常执行");
throw new Error("发生错误");
})
.then(() => {
// 这个 then 由于前一个 then 抛出了错误,所以被跳过
console.log("第二个 then 正常执行");
})
.then(() => {
// 这个 then 也被跳过,因为错误还没有被处理
console.log("第三个 then 正常执行");
})
.catch(error => {
console.log("捕获到错误:", error.message);
})
.then(() => {
// 这个 then 会执行,因为前面的 catch 已经处理了错误
console.log("在 catch 后的 then 正常执行");
});

输出结果:

1
2
3
第一个 then 正常执行
捕获到错误: 发生错误
在 catch 后的 then 正常执行

解析:

  1. **第一个 .then()**:正常执行并打印出消息。然后它抛出一个错误。
  2. **第二和第三个 .then()**:这些回调被跳过,因为它们在错误链路中紧随一个抛出错误的 .then()
  3. **.catch()**:捕获前一个 .then() 抛出的错误。处理完错误后,链路恢复正常执行。
  4. **最后一个 .then()**:在 .catch() 之后,由于错误已经被处理,所以这个 .then() 正常执行。

错误处理的重要性

Promise 链中,了解如何正确地处理错误是非常重要的。未被处理的错误不仅会阻止后续的 .then() 方法执行,还可能导致在实际应用中难以追踪的问题。一旦加入 .catch(),就可以恢复链条中后续的正常操作。

总之,如果在 Promise 链中的 .then() 方法中发生了错误,后续的 .then() 方法会被跳过,直到错误被 .catch() 方法捕获。如果希望即使出现错误也继续执行某些操作,可以在捕获错误后的 .catch() 方法之后继续链接 .then() 方法。这样的设计使得 Promise 非常适合处理异步流中可能出现的错误和异常情况。

https://mp.weixin.qq.com/s/oJas0DfSXhAqJ-u63HZD0g