DrillLab
第 06 / 25 节LESSON 06 / 25约 24 分钟~24 min

异步与事件循环六问6 questions on async and the event loop

事件循环、async/await vs Promise、回调地狱、finally、错误处理、异步方案总览。The event loop, async/await vs Promise, callback hell, finally, error handling, and an overview of the async options.

面试 · 第 3 部分Interview · Part 3
这一页有什么On this page7
学完这节你会After this lesson you can
  • 说出宏任务和微任务的执行顺序,并推出一段代码的输出State the order in which macrotasks and microtasks run, and work out what a piece of code prints
  • 说清 async/await 只是 Promise 的语法糖以及它带来的实际差别Explain that async/await is only syntax sugar over Promise, and what it changes in practice
  • 在四种并发场景下选对 Promise.all / allSettled / race / anyPick the right one of Promise.all / allSettled / race / any in four concurrency cases
  • 说明为什么 try/catch 抓不到异步错误Explain why try/catch does not catch an async error
这在考试里考什么What the exam does with this

事件循环是最能分出层次的一道题:只会说「JS 是单线程、异步靠回调」是及格,能背出「同步 → 微任务 → 渲染 → 宏任务」并解释 await 之后的代码是微任务,才是好答案。Promise 的四个静态方法几乎必被追问。The event loop question separates candidates more than any other. Saying that JavaScript is single threaded and does async work through callbacks is a pass. A good answer names the order — synchronous code, then microtasks, then paint, then one macrotask — and explains that the code after await runs as a microtask. The four static methods on Promise are almost always the follow-up.

§01

事件循环是怎么工作的How does the event loop work?

#305 What does the event loop

一句话:JS只有一个主线程, 事件循环负责在「调用栈空了」的时候, 从任务队列里取下一个任务放上去执行。

完整的一轮(这段是标准答案):

  1. 执行完当前的同步代码(调用栈清空)。
  2. 把微任务队列全部清空—— 注意是「全部」,而且清微任务时新产生的微任务也在这一轮里执行完
  3. (浏览器)需要的话渲染一帧。
  4. 一个宏任务执行,回到第 2 步。

谁是微任务:Promise.then/catch/finallyawait 之后的代码、queueMicrotaskMutationObserver
谁是宏任务:setTimeout / setInterval、 DOM 事件回调、网络回调、requestAnimationFrame(严格说它在渲染前,单独一档)。

一句话记住优先级:Promise 一定比setTimeout 先跑, 即使 setTimeout(…, 0)

会追问:「异步是谁做的?」—— 不是引擎, 是宿主环境(浏览器的 Web API / Node 的 libuv)。引擎只管执行 JS。 这条和 #276 是一组。
setTimeout(fn, 0)真的 0 毫秒吗?」—— 不是,浏览器最小约 4ms, 而且要等主线程空闲。所以它只是「尽快,但不是现在」。

Node 的差别(问到就是加分): Node 的宏任务分了六个阶段 (timers / pending / poll / check / close…),setImmediate 在 check 阶段,process.nextTick比所有微任务都优先

In one line: JS has one main thread, and the event loop’s job is to pull the next task off a queue and put it on the stack whenever the call stack goes empty.

One full turn — this part is the model answer:

  1. Run the synchronous code to the end (the call stack empties).
  2. Drain the microtask queue completely — note “completely”: microtasks queued while draining also run inside this same turn.
  3. (In a browser) paint a frame if one is needed.
  4. Take one macrotask, run it, go back to step 2.

Microtasks: Promise.then/catch/finally, the code after an await, queueMicrotask, MutationObserver.
Macrotasks: setTimeout / setInterval, DOM event handlers, network callbacks, requestAnimationFrame (strictly it runs just before paint, in a class of its own).

The priority in one line: a Promise always runs before a setTimeout, even setTimeout(…, 0).

Follow-up: “Who actually does the async work?” — not the engine, the host environment (the browser’s Web APIs, libuv in Node). The engine only runs JS. This one pairs with #276.
“Is setTimeout(fn, 0) really zero milliseconds?” — no. Browsers clamp it to roughly 4ms, and it still waits for a free main thread. So it means “as soon as possible, but not now”.

How Node differs (a bonus point if it comes up): Node splits macrotasks into six phases (timers / pending / poll / check / close…), setImmediate lands in the check phase, and process.nextTick jumps ahead of every microtask.

JavaScript必须能推出来的那道题The question you have to be able to work out示意Illustrative
1console.log("1 同步");
2
3setTimeout(() => console.log("2 宏任务"), 0);
4
5Promise.resolve().then(() => console.log("3 微任务"));
6
7(async () => {
8 console.log("4 同步(await 之前是同步的)");
9 await null;
10 console.log("5 微任务(await 之后)");
11})();
12
13console.log("6 同步");
14
15// 输出:1 同步 -> 4 同步 -> 6 同步 -> 3 微任务 -> 5 微任务 -> 2 宏任务
16//
17// 关键两点:
18// · async 函数体在遇到第一个 await 之前是同步执行的
19// · await 之后的代码等价于 .then 里的代码,是微任务
1console.log("1 sync");
2
3setTimeout(() => console.log("2 macrotask"), 0);
4
5Promise.resolve().then(() => console.log("3 microtask"));
6
7(async () => {
8 console.log("4 sync (everything before await is sync)");
9 await null;
10 console.log("5 microtask (after await)");
11})();
12
13console.log("6 sync");
14
15// Output: 1 sync -> 4 sync -> 6 sync -> 3 microtask -> 5 microtask -> 2 macrotask
16//
17// The two key points:
18// · the body of an async function runs synchronously until the first await
19// · the code after await is the same as code inside .then, so it is a microtask
面试给的题基本是这个变体。抓住两条:同步先跑完;微任务在宏任务前,且一次清空。The question you get in an interview is almost always a variant of this one. Hold on to two rules: all synchronous code runs first; microtasks run before macrotasks, and the whole microtask queue is drained at once.
§02

async/await vs Promise

#306 Async/await vs Promise

一句话:async/await是 Promise 的语法糖—— 同一套机制,但把「链式回调」写成了 「像同步一样往下读」。async 函数 永远返回一个 Promise。

.then()async/await
可读性嵌套一深就难读线性,好读
错误处理.catch()普通 try/catch——和同步代码统一了
调试断点难打,栈信息乱能逐行断点,栈清楚
中间变量要靠嵌套或额外传参才能共享就是普通局部变量
并发天然并行(先建好再 all)容易写成串行—— 这是最常见的性能错误

那个「容易写成串行」的坑值得单独说: 两个互不依赖的请求, 写成两行 await就变成了「等第一个回来再发第二个」, 总耗时是两者之和。正确做法是先都发出去,再一起await Promise.all

会追问:「什么时候还是用 .then 更好?」—— 只需要一步、不需要中间变量时; 或者要故意不等(fire and forget)。 另外 .then在需要把 Promise 存起来传递时更自然。

In one line: async/await is syntax sugar over Promises — the same machinery, but a chain of callbacks now reads straight down like synchronous code. An async function always returns a Promise.

A .then() chainasync/await
ReadabilityHard to follow the moment it nestsLinear, easy to read
Error handling.catch()Plain try/catch the same as synchronous code
DebuggingBreakpoints are awkward, stacks are a messStep line by line, clean stacks
Intermediate valuesShared only by nesting or passing them alongJust ordinary local variables
ConcurrencyParallel by nature (build them, then all)Easy to make serial by accident — the most common performance mistake there is

That “serial by accident” trap is worth its own sentence: two requests that do not depend on each other, written as two await lines, turn into “wait for the first, then send the second”, so the total is the sum of both. Fire them both off first, then await Promise.all.

Follow-up: “When is .then still the better choice?” — for a single step with no intermediate values, or when you deliberately do not want to wait (fire and forget). .then also reads better when you are storing a Promise and passing it around.

JavaScriptasync/await 最常见的性能错误The most common performance mistake with async/await示意Illustrative
1// ✗ 串行:总耗时 = a + b
2const user = await fetchUser(); // 等 200ms
3const posts = await fetchPosts(); // 再等 200ms -> 共 400ms
4
5// ✓ 并行:总耗时 = max(a, b)
6const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
7// ↑ 两个请求同时发出去 -> 共 200ms
8
9// 有依赖时串行才是对的
10const user = await fetchUser();
11const posts = await fetchPosts(user.id); // 必须先有 user.id
1// ✗ One after the other: total time = a + b
2const user = await fetchUser(); // wait 200ms
3const posts = await fetchPosts(); // wait another 200ms -> 400ms in total
4
5// ✓ Side by side: total time = max(a, b)
6const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
7// ↑ both requests go out at once -> 200ms total
8
9// When one depends on the other, going one at a time is correct
10const user = await fetchUser();
11const posts = await fetchPosts(user.id); // user.id has to exist first
§03

什么是回调地狱What is callback hell?

#307 What is callback hell

一句话:异步一步依赖上一步时, 回调套回调,缩进越来越深, 形成一个横着的三角形 —— 也叫「厄运金字塔」。

它真正的问题不只是难看,有三条:

  • 错误处理要写 n 遍。每一层都得判 if (err), 漏一个就静默失败。
  • 没法组合。想改成「这两步并行」几乎要重写。
  • 控制流全靠缩进表达, 加一步要改一堆括号。

怎么解,按历史顺序:命名函数拆平 → Promise 链 (把嵌套变成链式,错误集中到一个.catch)→async/await(彻底变线性)。

会追问:「Promise 解决了回调地狱的哪个问题?」——主要是「错误处理」和「组合」, 缩进只是顺带。 能这么答说明你理解本质, 而不是「Promise 让代码变平了」这种表面回答。

In one line: when each async step depends on the last, callbacks nest inside callbacks, the indentation keeps growing, and you end up with a sideways triangle — also called the pyramid of doom.

The real damage is not that it looks bad. There are three problems:

  • Error handling gets written n times. Every level needs its own if (err), and one missed check is a silent failure.
  • Nothing composes. Making two of those steps run in parallel means rewriting nearly all of it.
  • Control flow is expressed only by indentation, so inserting a step means shuffling a pile of braces.

The fixes, in historical order: pull the callbacks out into named functions → a Promise chain (nesting becomes chaining, errors collapse into one .catch) → async/await, which makes it fully linear.

Follow-up: “Which part of callback hell did Promises actually solve?” — mostly error handling and composition; the indentation came along for the ride. Answering that way shows you understand the substance, not just “Promises flatten the code”.

JavaScript同一段逻辑的两种写法The same logic written two ways示意Illustrative
1// 回调地狱
2getUser(id, (err, user) => {
3 if (err) return handle(err);
4 getPosts(user.id, (err, posts) => {
5 if (err) return handle(err); // 又来一遍
6 getComments(posts[0].id, (err, comments) => {
7 if (err) return handle(err); // 再来一遍
8 render(comments);
9 });
10 });
11});
12
13// async/await
14try {
15 const user = await getUser(id);
16 const posts = await getPosts(user.id);
17 const comments = await getComments(posts[0].id);
18 render(comments);
19} catch (err) {
20 handle(err); // 一处兜住全部
21}
1// Callback hell
2getUser(id, (err, user) => {
3 if (err) return handle(err);
4 getPosts(user.id, (err, posts) => {
5 if (err) return handle(err); // the same line again
6 getComments(posts[0].id, (err, comments) => {
7 if (err) return handle(err); // and once more
8 render(comments);
9 });
10 });
11});
12
13// async/await
14try {
15 const user = await getUser(id);
16 const posts = await getPosts(user.id);
17 const comments = await getComments(posts[0].id);
18 render(comments);
19} catch (err) {
20 handle(err); // one place catches all of them
21}
§04

Promise 链里的 finally() 有什么用What is finally() for in a Promise chain?

#309 What is the purpose of the finally() method in a Promise chain

一句话:无论成功还是失败都会执行, 用来做收尾 —— 关 loading、 释放资源、上报耗时。

三个性质要说清:

  • 拿不到值也拿不到错误—— 回调不接参数。它的定位就是「不关心结果的清理」。
  • 透传—— 它把原来的值或错误原样往下传, 不影响链的状态。 所以 finallyreturn一个值不会改变结果
  • 但它里面抛错会覆盖原来的结果—— 这是唯一能改变链状态的方式。

为什么这题值得问:因为它对应一个 真实 bug —— 只在成功路径里setLoading(false), 出错时界面就永远卡在 Loading。我们那道 fetch 变式题的常见错误里就有这一条。

会追问:「和 try/catch/finallyfinally 一样吗?」—— 语义一样,都是「一定执行」。async/await 里直接用try/finally 就行,不用.finally()

In one line: it runs whether the promise succeeded or failed, so it is where the cleanup goes — turn off loading, release resources, report how long it took.

Three properties to state clearly:

  • It sees neither the value nor the error — the callback takes no arguments. Its whole job is cleanup that does not care about the result.
  • It passes through — the original value or error continues down the chain unchanged, so the state of the chain is untouched. A return inside finally changes nothing.
  • But throwing inside it does override the result — that is the only way it can change the chain.

Why this question earns its place: it maps onto a real bug — call setLoading(false) only on the success path and the UI sits on Loading forever when the request fails. That exact mistake is on the common-errors list for our fetch variant question.

Follow-up: “Is it the same as the finally in try/catch/finally?” — same meaning: it always runs. With async/await just use try/finally; you do not need .finally().

JavaScriptfinally 的三个性质Three properties of finally示意Illustrative
1fetch(url)
2 .then((r) => r.json())
3 .then(setData)
4 .catch(setError)
5 .finally(() => setLoading(false)); // 成功失败都要关 loading
6
7// 等价的 async 写法
8try {
9 const r = await fetch(url);
10 setData(await r.json());
11} catch (e) {
12 setError(e.message);
13} finally {
14 setLoading(false); // ← 放这里,别只放在 try 末尾
15}
16
17// finally 透传,return 不生效
18Promise.resolve(1).finally(() => 99).then(console.log); // 1,不是 99
19// 但抛错会覆盖
20Promise.resolve(1).finally(() => { throw new Error("x"); }).catch(e => console.log(e.message)); // "x"
1fetch(url)
2 .then((r) => r.json())
3 .then(setData)
4 .catch(setError)
5 .finally(() => setLoading(false)); // turn loading off whether it worked or not
6
7// The same thing written with async
8try {
9 const r = await fetch(url);
10 setData(await r.json());
11} catch (e) {
12 setError(e.message);
13} finally {
14 setLoading(false); // ← put it here, not only at the end of try
15}
16
17// finally passes the value through; its return value is ignored
18Promise.resolve(1).finally(() => 99).then(console.log); // 1, not 99
19// But throwing does replace it
20Promise.resolve(1).finally(() => { throw new Error("x"); }).catch(e => console.log(e.message)); // "x"
§05

错误处理怎么做How do you handle errors?

#310 Error Handling

一句话:同步用try/catch, Promise 用 .catch()async/awaittry/catch最上层要有兜底

最重要的一条:try/catch抓不到「回调里」的异步错误。因为 setTimeout的回调是在另一轮事件循环里执行的, 那时 try 块早就出栈了。

四个层次的实践:

  • 该抛就抛—— 别把错误吞掉换成return null, 调用方无法区分「没有」和「出错了」。
  • Error 对象, 别抛字符串 —— 否则没有堆栈。 需要区分类型就class NotFoundError extends Error
  • 只在能处理的地方 catch。 catch 了却什么都不做(catch (e) {}) 是最坏的写法。
  • 兜底—— 浏览器 window.onerror +unhandledrejection; Node process.on("uncaughtException"); React 用错误边界(见 #333); Express 用错误中间件

会追问:「fetch 的错误怎么处理?」——陷阱题fetch 只在网络层失败时 reject,404 / 500 是 resolve 的, 必须自己检查 res.ok。 这也是 React 那门课里 fetch 变式题的第一个考点。

In one line: try/catch for synchronous code, .catch() for Promises, try/catch again for async/await; and always keep a backstop at the very top.

The most important point: try/catch cannot catch an async error thrown inside a callback. A setTimeout callback runs in a later turn of the event loop, and by then the try block is long off the stack.

Four levels of practice:

  • Throw when you should throw — do not swallow the error and hand back return null; the caller then cannot tell “not there” from “it broke”.
  • Throw an Error object, never a string — a string carries no stack. When callers need to tell cases apart, write class NotFoundError extends Error.
  • Only catch where you can actually handle it. Catching and then doing nothing (catch (e) {}) is the worst thing you can write.
  • Keep a backstop — in the browser window.onerror plus unhandledrejection; in Node process.on("uncaughtException"); in React an error boundary (see #333); in Express error-handling middleware.

Follow-up: “How do you handle errors from fetch?” — this is a trick question. fetch only rejects when the network layer fails; 404 and 500 both resolve, so you have to check res.ok yourself. That is also the first thing the fetch variant question in the React course tests.

JavaScript四条实践Four practices示意Illustrative
1// ✗ 抓不到 —— 回调在下一轮事件循环里跑
2try {
3 setTimeout(() => { throw new Error("炸了"); }, 0);
4} catch (e) {
5 console.log("抓不到这里");
6}
7
8// ✓ 异步错误要在异步链里抓
9try {
10 await somethingAsync();
11} catch (e) { /* ✓ */ }
12
13// ✗ 吞掉错误,调用方分不清「没有」还是「出错」
14async function getUser(id) {
15 try { return await api.get(id); }
16 catch { return null; }
17}
18
19// ✓ 让它抛,或者抛一个带类型的错误
20class NotFoundError extends Error {}
21if (!row) throw new NotFoundError(`user ${id} 不存在`);
22
23// 兜底
24window.addEventListener("unhandledrejection", (e) => report(e.reason));
1// ✗ Never caught —— the callback runs in a later turn of the event loop
2try {
3 setTimeout(() => { throw new Error("failed"); }, 0);
4} catch (e) {
5 console.log("this line is never reached");
6}
7
8// ✓ Catch an async error inside the async chain
9try {
10 await somethingAsync();
11} catch (e) { /* ✓ */ }
12
13// ✗ Swallowing the error: the caller cannot tell "not there" from "it failed"
14async function getUser(id) {
15 try { return await api.get(id); }
16 catch { return null; }
17}
18
19// ✓ Let it throw, or throw an error that has a type
20class NotFoundError extends Error {}
21if (!row) throw new NotFoundError(`user ${id} not found`);
22
23// A last line of defence
24window.addEventListener("unhandledrejection", (e) => report(e.reason));
§06

怎么处理异步操作How do you handle asynchronous operations?

#311 Handle asynchronous operations

一句话(按历史讲最清楚):回调 → Promise → async/await, 外加事件和 for await…of处理流式数据。

但这题真正的考点是 Promise 的四个静态方法怎么选, 一定会追问:

方法什么时候 resolve用在哪
all全部成功才成功,一个失败立刻失败几个都必须成功(页面必需的多个接口)
allSettled全部结束就成功, 不管成败批量操作,要知道每一个的结果 (批量上传,报告哪几个失败了)
race第一个结束的说话, 成功失败都算超时控制
any第一个成功的, 全失败才失败多个镜像源取最快能用的那个

all 的坑: 它是 fail-fast 的 —— 一个失败,其他已经在飞的请求不会被取消, 而且你拿不到其他的结果。 「批量操作要逐个报告」的场景该用allSettled

会追问:「怎么给一个请求加超时?」——Promise.race配一个定时 reject 的 Promise; 更好的是用 AbortController, 因为它能真的把请求掐掉race 只是不等了。 这两个的区别在我们那道 fetch 变式题里也讲过。

In one line, and history tells it best: callbacks → Promises → async/await, plus events and for await…of for streaming data.

But what this question is really after is how you pick among the four static Promise methods, and they will ask:

MethodWhen it resolvesWhere you use it
allSucceeds only if all of them succeed, and fails the instant one failsEverything must succeed (several calls the page needs)
allSettledSucceeds once everything has finished, win or loseBulk work where you need each result (a batch upload, reporting which ones failed)
raceWhoever finishes first speaks, success or failureTimeouts
anyThe first success; it fails only if all failSeveral mirrors, take the fastest one that works

The trap in all: it is fail-fast — one rejection and the other requests already in flight are not cancelled, and you never see their results. When bulk work has to report item by item, reach for allSettled.

Follow-up: “How do you put a timeout on a request?” — Promise.race against a Promise that rejects on a timer. Better is AbortController, because it actually kills the request, whereas race merely stops waiting. We walk through that difference in the fetch variant question too.

JavaScript四个方法与超时The four methods, and timeouts示意Illustrative
1// 都必须成功
2const [user, posts] = await Promise.all([getUser(), getPosts()]);
3
4// 要逐个知道结果
5const results = await Promise.allSettled(files.map(upload));
6const failed = results.filter((r) => r.status === "rejected");
7
8// 超时:race 只是「不等了」,请求还在飞
9const withTimeout = (p, ms) =>
10 Promise.race([
11 p,
12 new Promise((_, rej) => setTimeout(() => rej(new Error("超时")), ms)),
13 ]);
14
15// 更好:AbortController 真的掐掉请求
16const c = new AbortController();
17setTimeout(() => c.abort(), 5000);
18await fetch(url, { signal: c.signal });
1// All of them have to succeed
2const [user, posts] = await Promise.all([getUser(), getPosts()]);
3
4// You need the result of each one
5const results = await Promise.allSettled(files.map(upload));
6const failed = results.filter((r) => r.status === "rejected");
7
8// Timeout: race only means "stop waiting"; the request is still in flight
9const withTimeout = (p, ms) =>
10 Promise.race([
11 p,
12 new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
13 ]);
14
15// Better: AbortController really does cancel the request
16const c = new AbortController();
17setTimeout(() => c.abort(), 5000);
18await fetch(url, { signal: c.signal });
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

给代码问输出顺序Handed code and asked in what order it prints
同步 → 微任务全清 → 一个宏任务;Promise 先于 setTimeoutSynchronous code, then every microtask, then one macrotask; a Promise runs before setTimeout
两个 await 连着写Two awaits written one after the other
检查是否该改成 Promise.all 并行Check whether they should run at the same time with Promise.all
「出错后卡在 Loading」After an error the screen is stuck on Loading
setLoading(false) 放 finallyPut setLoading(false) in finally
try/catch 抓不到错误try/catch does not catch the error
错误在回调里,已经是下一轮事件循环The error is inside a callback, which already runs on a later turn of the event loop
批量操作要报告每一个A batch job has to report on every item
allSettled,不是 allallSettled, not all
要超时You need a timeout
race 是不等了,AbortController 才是真取消race only stops waiting; AbortController is what actually cancels
fetch 拿到 404 却当成功fetch gets a 404 and treats it as success
自己检查 res.okCheck res.ok yourself
这节的要点What to take away
  1. 事件循环一轮:同步跑完 → 微任务一次全清 → 渲染 → 取一个宏任务;异步能力来自宿主环境不是引擎。One turn of the event loop: run all synchronous code, drain every microtask, paint, then take one macrotask; the async ability comes from the host environment, not from the engine.
  2. async 函数体在第一个 await 前是同步的;await 之后等价于 .then,属微任务。The body of an async function runs synchronously up to the first await; what comes after await is the same as .then, so it is a microtask.
  3. async/await 是 Promise 语法糖,最大好处是错误处理和同步代码统一;最大坑是把并行写成串行。async/await is syntax sugar over Promise; the main gain is that errors are handled the same way as in synchronous code, and the main mistake is writing work in sequence when it could run at the same time.
  4. 回调地狱真正的问题是错误处理要写 n 遍和无法组合,不只是缩进深。The real problem with callback hell is that error handling is repeated in every callback and the steps cannot be composed, not just that the indentation grows.
  5. finally 拿不到值、原样透传、但里面抛错会覆盖结果;关 loading 就该放这儿。finally receives no value and passes the result through unchanged, but an error thrown inside it replaces that result; turning loading off belongs here.
  6. try/catch 抓不到回调里的异步错误;抛 Error 对象不抛字符串;最外层要有兜底。try/catch does not catch an async error raised inside a callback; throw an Error object, not a string; and keep one handler at the outermost level.
  7. all 全成功、allSettled 全结束、race 第一个结束、any 第一个成功。all needs every one to succeed, allSettled waits for every one to finish, race takes the first one to finish, any takes the first one to succeed.

接下来What next

  1. 接着看下一节Continue to the next lessonDOM、模块与工具链七问7 questions on the DOM, modules and tooling
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 6 道八股6 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: this 与面向对象三问3 questions on this and object-oriented programming