Skip to content

Commit 3d3ffdd

Browse files
committed
update
1 parent d3bd2f1 commit 3d3ffdd

39 files changed

+3671
-1118
lines changed

docs/chapters/answers.md

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# 练习答案与提示
2+
3+
> 说明:以下为参考答案/提示,不是唯一解。为了保持可读性,示例代码尽量短,省略了异常处理。
4+
5+
## 第二章 计算
6+
7+
**练习 1:懒求值下的乘法次数**
8+
9+
`pow5(x)` 展开是 `x*x*x*x*x`,需要 4 次乘法。如果参数不先求值,每一次出现都会重新计算。
10+
11+
`M(n)` 表示 `pow5` 嵌套 n 次的乘法次数:
12+
13+
* `M(1) = 4`
14+
* `M(n) = 4 + 5 * M(n-1)`
15+
16+
因此:`M(2)=24``M(3)=124``M(4)=624`
17+
18+
**练习 2:强制求值的问题**
19+
20+
* 可能做很多无用计算(性能浪费)。
21+
* 不能处理“无穷序列/无穷结构”。
22+
* 即使分支不会被用到,也提前执行(丢失短路优势)。
23+
* 如果参数包含副作用,提前执行会改变行为。
24+
25+
## 第三章 过程
26+
27+
**阶乘(循环/递归/尾递归)**
28+
29+
```javascript
30+
function fact(n) {
31+
let r = 1;
32+
for (let i = 2; i <= n; i++) r *= i;
33+
return r;
34+
}
35+
36+
function factRec(n) {
37+
return n <= 1 ? 1 : n * factRec(n - 1);
38+
}
39+
40+
function factTail(n, acc = 1) {
41+
return n <= 1 ? acc : factTail(n - 1, acc * n);
42+
}
43+
```
44+
45+
**斐波那契(循环/递归/尾递归)**
46+
47+
```javascript
48+
function fib(n) {
49+
let a = 0, b = 1;
50+
for (let i = 0; i < n; i++) {
51+
[a, b] = [b, a + b];
52+
}
53+
return a;
54+
}
55+
56+
function fibRec(n) {
57+
return n <= 1 ? n : fibRec(n - 1) + fibRec(n - 2);
58+
}
59+
60+
function fibTail(n, a = 0, b = 1) {
61+
return n === 0 ? a : fibTail(n - 1, b, a + b);
62+
}
63+
```
64+
65+
**短路求值说明**
66+
67+
`condition ? expr1 : expr2` 只会计算其中一个分支。
68+
所以当 `y == 1` 时,`pow(x, y-1) * x` 不会被执行。
69+
70+
**顺序/分支/循环的现实过程**
71+
72+
示例:
73+
74+
* 顺序:起床 → 洗漱 → 早餐。
75+
* 分支:是否下雨?下雨带伞,否则不带。
76+
* 循环:刷牙 2 分钟(重复动作)。
77+
78+
## 第四章 编码
79+
80+
**小数的表示**
81+
82+
`1/3` 在十进制是无限循环小数,到了二进制依然是无限循环。IEEE 754 用“符号位 + 指数 + 尾数”近似表示,所以会出现精度误差。
83+
84+
可以用 `0.1 + 0.2` 观察误差:
85+
86+
```javascript
87+
0.1 + 0.2; // 0.30000000000000004
88+
```
89+
90+
## 第五章 序列
91+
92+
**LCS 提示**
93+
94+
经典解法是动态规划:
95+
96+
```
97+
if a[i-1] == b[j-1] => dp[i][j] = dp[i-1][j-1] + 1
98+
else => dp[i][j] = max(dp[i-1][j], dp[i][j-1])
99+
```
100+
101+
这要求你能“多次访问任意位置”,所以在 C++ 中至少需要 Random Access 迭代器。
102+
103+
## 第六章 数据
104+
105+
**sumList**
106+
107+
```javascript
108+
function sumList(list) {
109+
let sum = 0;
110+
for (let p = list; p !== null; p = p.next) sum += p.value;
111+
return sum;
112+
}
113+
```
114+
115+
**filterList**
116+
117+
```javascript
118+
function filterList(list, pred) {
119+
if (list === null) return null;
120+
if (pred(list.value)) {
121+
return { value: list.value, next: filterList(list.next, pred) };
122+
}
123+
return filterList(list.next, pred);
124+
}
125+
```
126+
127+
**树遍历**
128+
129+
参考正文中的 `preorder/inorder/postorder`
130+
131+
**DFS/BFS**
132+
133+
参考正文中的 `dfs/bfs`,DFS 使用栈,BFS 使用队列。
134+
135+
## 第七章 状态
136+
137+
**计数器**
138+
139+
```javascript
140+
let counter = {
141+
value: 0,
142+
inc() { this.value += 1; },
143+
dec() { this.value -= 1; }
144+
};
145+
```
146+
147+
**电梯状态机**
148+
149+
状态示例:`idle / up / down / open`,转移规则取决于请求队列。
150+
151+
**move**
152+
153+
```javascript
154+
function move(point, dx, dy) {
155+
return { x: point.x + dx, y: point.y + dy };
156+
}
157+
```
158+
159+
**共享状态与库存示例(提示)**
160+
161+
把“读库存 + 减库存”拆成两步,再用 `await` 人为制造切换点,就能看到负库存或丢失更新的情况:
162+
163+
```javascript
164+
let stock = 1;
165+
async function buy() {
166+
let current = stock;
167+
await Promise.resolve();
168+
stock = current - 1;
169+
}
170+
```
171+
172+
## 第八章 引用
173+
174+
**数组输出**
175+
176+
`a``b` 指向同一数组,`c` 是浅拷贝:
177+
178+
```javascript
179+
// 输出:
180+
// a: [1, 2, 3]
181+
// b: [1, 2, 3]
182+
// c: [1, 2]
183+
```
184+
185+
**cloneUser(浅拷贝)**
186+
187+
```javascript
188+
const cloneUser = user => ({ ...user });
189+
```
190+
191+
**freeze 示例**
192+
193+
```javascript
194+
let obj = Object.freeze({ x: 1 });
195+
obj.x = 2;
196+
obj.x; // 1
197+
```
198+
199+
**共享尾部链表(提示)**
200+
201+
两个链表可以共用同一个尾节点:
202+
203+
```javascript
204+
let tail = node(3, null);
205+
let listA = node(1, node(2, tail));
206+
let listB = node(9, tail);
207+
```
208+
209+
优点:节省内存;风险:修改共享节点会影响两条链表。
210+
211+
## 第九章 闭包
212+
213+
**makeOnce**
214+
215+
```javascript
216+
function makeOnce(fn) {
217+
let called = false;
218+
return function (...args) {
219+
if (called) return;
220+
called = true;
221+
return fn(...args);
222+
};
223+
}
224+
```
225+
226+
**makeTimer**
227+
228+
```javascript
229+
function makeTimer() {
230+
const start = Date.now();
231+
return () => Date.now() - start;
232+
}
233+
```
234+
235+
**outer()() 为什么能输出**
236+
237+
因为返回的函数闭包捕获了 `msg`,即使 `outer` 结束了,`msg` 仍然可访问。
238+
239+
## 第十章 对象
240+
241+
**Animal / Dog / Cat**
242+
243+
```javascript
244+
class Animal {
245+
speak() { console.log('...'); }
246+
}
247+
class Dog extends Animal {
248+
speak() { console.log('woof'); }
249+
}
250+
class Cat extends Animal {
251+
speak() { console.log('meow'); }
252+
}
253+
```
254+
255+
**feed**
256+
257+
```javascript
258+
function feed(animal) {
259+
animal.eat();
260+
}
261+
```
262+
263+
**组合 move + draw**
264+
265+
```javascript
266+
let canMove = { move() { console.log('move'); } };
267+
let canDraw = { draw() { console.log('draw'); } };
268+
let sprite = Object.assign({}, canMove, canDraw);
269+
```
270+
271+
## 第十一章 并发
272+
273+
**修复银行竞态(队列化)**
274+
275+
```javascript
276+
function createQueue() {
277+
let last = Promise.resolve();
278+
return function enqueue(task) {
279+
last = last.then(() => task()).catch(() => {});
280+
return last;
281+
};
282+
}
283+
284+
let enqueue = createQueue();
285+
286+
enqueue(() => deposit(200));
287+
enqueue(() => withdraw(200));
288+
```
289+
290+
**顺序打印 1/2/3**
291+
292+
```javascript
293+
function delay(ms) {
294+
return new Promise(resolve => setTimeout(resolve, ms));
295+
}
296+
297+
(async function () {
298+
for (let i = 1; i <= 3; i++) {
299+
await delay(1000);
300+
console.log(i);
301+
}
302+
})();
303+
```
304+
305+
**任务队列(提示)**
306+
307+
把每个任务包装成返回 Promise 的函数,然后用一个“串行链”依次执行。

docs/chapters/ch01_environment.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ JavaScript的环境触手可及。在电脑端你可以随时打开一个现代
4141

4242
MDN 中对[浏览器开发者工具](https://developer.mozilla.org/zh-CN/docs/Learn/Discover_browser_developer_tools)[基本的工具软件](https://developer.mozilla.org/zh-CN/docs/Learn/Getting_started_with_the_web/Installing_basic_software)有更为完整的介绍,可以参考。如果你想了解更多关于Web设计和开发的知识,MDN也有完备的文档供参考。
4343

44+
## 延伸阅读
45+
46+
* [进程与线程 Process & Thread](../reference/process-and-thread.md)
47+
* [阻塞与非阻塞 Blocking & Non-blocking](../reference/blocking-nonblocking.md)
48+
* [同步与异步 Sync & Async](../reference/sync-async.md)
49+
* [调用栈 Call Stack](../reference/call-stack.md)
50+
4451
一切准备就绪的话,我们就开始吧。

docs/chapters/ch02_computation.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,32 @@ $$
134134

135135
\*请记住我们现在讨论的这个问题,后面会展开并且深入地探讨。
136136

137+
### 补充:函数的可组合性
138+
139+
函数的组合不仅仅是数学里的“代入”,也可以用于日常操作的拼装。比如我们想要“排序后再反转”的结果:
140+
141+
```javascript
142+
function sorted(array) {
143+
return array.slice().sort();
144+
}
145+
146+
function reversed(array) {
147+
return array.slice().reverse();
148+
}
149+
150+
function sortReversed(array) {
151+
return reversed(sorted(array));
152+
}
153+
```
154+
155+
组合的意义在于:**用小而清晰的步骤,拼出更复杂的行为**。如果你想对数字排序,需要提供比较器(后面“作为数据的过程”会讲到)。
156+
137157
## 练习
138158

139159
* 请思考,如果函数的参数在代入函数之前并不进行强制求值,`pow5(pow5(pow5(pow5(5))))`展开后共计算多少次乘法。
140160
* 强制求值可能会造成的问题有哪些。
141161

162+
## 延伸阅读
163+
164+
* [复杂度 Complexity](../reference/complexity.md)
165+
* [浮点数 Floating Point](../reference/floating-point.md)

docs/chapters/ch03_procedure.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ function pow(x, y, prod = 1) {
202202
}
203203
y = y - 1;
204204
prod*=x;
205-
return prow(x, y, prod);
205+
return pow(x, y, prod);
206206
}
207207
```
208208

@@ -230,6 +230,23 @@ function pow(x, y) {
230230
}
231231
```
232232

233+
### 递归树与效率(补充)
234+
235+
递归在表达结构上很自然,但有时会重复计算。
236+
237+
以斐波那契为例(常用定义:`fib(0)=0, fib(1)=1`):
238+
239+
```javascript
240+
function fib(n) {
241+
if (n <= 1) return n;
242+
return fib(n - 1) + fib(n - 2);
243+
}
244+
```
245+
246+
`n` 增大时,`fib(n-2)` 等子问题会被重复计算很多次,调用次数增长很快。把它改成尾递归或循环,可以避免这种重复。
247+
248+
> 提醒:JavaScript 规范并不保证“尾调用优化”,很多运行时也没有开启,所以尾递归更多是理解思路,实际工程里常常直接用循环。
249+
233250
## 短路求值
234251

235252
试想一下我们的递归版pow代码
@@ -272,3 +289,8 @@ function pow(x, y) {
272289
* 用以上代码解释短路求值
273290
* 请使用顺序、分支、循环来描述现实中的一些过程。
274291

292+
## 延伸阅读
293+
294+
* [递归 Recursion](../reference/recursion.md)
295+
* [调用栈 Call Stack](../reference/call-stack.md)
296+
* [尾调用优化 Tail Call Optimization](../reference/tail-call-optimization.md)

0 commit comments

Comments
 (0)