Skip to content

Commit 044cce9

Browse files
authored
Merge pull request #40 from abmfy/master
Merge 2022 summer training JS & TS docs
2 parents f4fda8b + 8c3d27a commit 044cce9

File tree

16 files changed

+1678
-330
lines changed

16 files changed

+1678
-330
lines changed

docs/frontend/react/prepare.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,14 @@ yarn start
8787

8888
如果你现在能够在 3000 端口访问到一个写有 `Hello, React!` 字样的页面,则已经完成了配置。
8989

90-
## 使用 ESLint 规范码风
90+
## 代码格式化
9191

9292
这是一个**可选项**,但是我们认为开发较大工程的时候,尤其是需要多人协作的时候,统一的码风是必要的。
9393

94+
即使每个人都可以有自己习惯的编写风格,但为了其他人阅读方便,在提交代码之前应该使用代码格式化工具统一码风。这里简单介绍两种常用的格式化工具。
95+
96+
### ESLint
97+
9498
在应用目录下执行下列命令,即可初步配置一个 ESLint:
9599

96100
```shell
@@ -100,7 +104,7 @@ npm install eslint --save-dev
100104

101105
# yarn
102106
yarn add eslint --dev
103-
./node_modules/.bin/eslint --init
107+
yarn eslint --init
104108
```
105109

106110
执行第二条命令后,ESLint 会询问你的配置。对于前几个问题我们建议选择“强制调整码风” “使用 JavaScript modules(import/export) ” “使用 React 框架” “使用 TypeScript ” “在浏览器上运行代码”。
@@ -129,4 +133,31 @@ yarn add eslint --dev
129133

130134
这一行的意思是执行 `yarn fix` 的时候会使用 ESLint 将 `src` 目录下的所有扩展名为 `js / jsx / ts / tsx` 的文件修复码风。
131135

132-
不过要注意的是,自动修复能力有限,所以更多的时候还需要手动对文件进行一些调整。自动修复也有可能会出错,这个时候可以自己手动添加让 ESLint 忽略这一部分的注释。
136+
不过要注意的是,自动修复能力有限,所以更多的时候还需要手动对文件进行一些调整。自动修复也有可能会出错,这个时候可以自己手动添加让 ESLint 忽略这一部分的注释。
137+
138+
### Prettier
139+
140+
在 Node.js 项目中添加 Prettier:
141+
142+
```bash
143+
yarn add -D prettier
144+
```
145+
146+
`package.json` 中添加:
147+
148+
```json
149+
{
150+
"scripts": {
151+
...
152+
"fix": "prettier --write 'src/**/*{.js,.jsx,.ts,.tsx}'"
153+
}
154+
}
155+
```
156+
157+
这里的含义与 ESLint 相似,只不过这次是由 Prettier 尝试修复码风。
158+
159+
!!! note "ESLint 和 Prettier 的区别"
160+
161+
需要注意的是,Prettier 仅仅尝试修复码风,它所进行的一切修改在语法上都是等价的,也即 Prettier 并不关心代码的正确性,甚至不会检查是否符合语法规范;同时 Prettier 可以配置的项目相当少,这是由于 Prettier 的开发者更希望能强制统一标准而非在具体 lint 配置上陷入无意义的争论。
162+
163+
ESLint 在检查风格的同时也可以进行更多的语法检查,还可以通过插件来检查更加具体的错误,例如 React Hook 的不当使用等;相比于 Prettier,ESLint 还可以对代码风格进行更复杂的配置。

docs/languages/javascript/async.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@ console.log("Loading local storage..."); // Main thread doing other tasks
7070

7171
### 回调函数的缺陷
7272

73+
前面已经提到,在 TypeScript 中常常会面临大量的回调函数,这往往是因为我们需要在被调用的函数中执行一些自定义的操作。这样做的理由可能十分多样,但一种很常见的原因是,被调用的函数本可以将数据返回给调用者去处理,但实际上这个数据可能并不会立即获取到。为了不阻塞整个程序,只能由调用者将后续操作封装为闭包后交给被调用函数,等到数据准备好时,通过调用闭包来完成后续操作。
74+
75+
例如在读取文件这一场景中:
76+
77+
```javascript
78+
// readFile 是 Node.js 内置的读取文件的方法
79+
import { readFile } from 'fs';
80+
81+
const handler = (err, data) => {
82+
console.log(data.toString());
83+
}
84+
readFile('~/someFile', handler);
85+
```
86+
87+
可以看到,由于读取文件需要时间,我们并不能一直等待读取结果,而是写了 handler 这个闭包告诉 readFile 当读取完成后应该做什么。
88+
7389
回调函数不需要引入很多的其他语法就可以方便地使用到异步之中,但是其问题也是很突出的。回调函数本身可读性就不是很好,而且也并不能很好贴合我们的直观思维逻辑。而其最大的问题就是可能造成回调函数过分嵌套,导致代码难以维护。这一般被称为**回调地狱**
7490

7591
真正的异步业务逻辑可能并不会很单一,比如说前端要从多个数据源加载数据,但是后一个数据的加载需要依靠前一个数据的结果。这样就不能在主线程中同时派遣多个异步过程,而必须在前一个异步过程的回调之中派遣下一个异步过程。这里我们依然用 `setTimeout` 来模拟耗时操作:
@@ -229,6 +245,8 @@ console.log(i); // 1
229245

230246
在最新标准之中,JavaScript 引入了 `async, await` 这两个关键字,这两个关键字的作用是能够让异步代码写得和同步代码一样自然。
231247

248+
直接使用 `Promise` 看起来有些复杂,尤其是在链式调用时,如果中间变量需要保存,写法就变得丑陋,因此在更多的时候,我们使用 `async` / `await` 关键词。
249+
232250
我们可以用 `async` 关键字将一个函数声明为异步函数。调用异步函数的时候,其会立刻返回并派遣一个异步:
233251

234252
```javascript
@@ -251,7 +269,7 @@ const foo = async () => {
251269
}, 1000);
252270
};
253271

254-
typeof foo(); // "object", note "undefined"
272+
typeof foo(); // "object", not "undefined"
255273
```
256274

257275
那么我们也可以按照 `Promise``then` 链写法使用异步函数:

docs/languages/javascript/control.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,91 @@ if (new Boolean(false)) {
2828
console.log("WTF?");
2929
} // "WTF!"
3030
```
31+
32+
值得注意的是 `for` 循环除了经典 C 风格外还可以实现类似 Python 的迭代方法。
33+
34+
### `for...in` 循环
35+
36+
`for...in` 语句以**任意顺序**迭代一个对象的除 `symbol` 以外的**可枚举**属性,包括继承的可枚举属性。关于可枚举性可参考[属性的可枚举性和所有权](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Enumerability_and_ownership_of_properties),继承将在对象部分展开讲解。
37+
38+
简单而言,使用 `for...in` 语句可以遍历对象的键:
39+
40+
```javascript
41+
const obj = { a: 1, b: 2 };
42+
for (const key in obj) {
43+
console.log(`${key}: ${obj[key]}`);
44+
}
45+
// a: 1
46+
// b: 2
47+
```
48+
49+
虽然这种方法也可以遍历数组的索引,但这并不是好的编码习惯,迭代数组应当使用数组的 `forEach` 方法或使用 `for...of` 语句。
50+
51+
### `for...of` 循环
52+
53+
`for...of` 循环在可迭代对象(如 `Array``Map``Set``String`,自己创建可迭代对象可以参考[迭代协议](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols))上创建迭代循环,最常见的是对数组元素进行迭代:
54+
55+
```javascript
56+
let frontend_courses = ['Java/Android', 'JS/TS/React'];
57+
for (const course of frontend_courses) {
58+
console.log(course);
59+
}
60+
// Java/Android
61+
// JS/TS/React
62+
```
63+
64+
### 异常处理
65+
66+
JavaScript 的异常处理与 C++ 相似:
67+
68+
```javascript
69+
try {
70+
someDangerousFunction();
71+
} catch (err) {
72+
console.log('error occurred');
73+
}
74+
```
75+
76+
### 断句
77+
78+
JavaScript 句末可以加分号也可以不加,但为了安全和便于阅读,建议总是在句末加分号。你可能会好奇为何会出现不安全的情况,请看这一示例:
79+
80+
```javascript
81+
// 假定 f 是一个函数,函数可以赋值给变量,这个将在下一节展开
82+
let a = 1
83+
let g = f
84+
(a)
85+
```
86+
87+
虽然单独写一个 `(a)` 看起来毫无意义,但这理论上是合法语句,只不过没有产生任何副作用,但实际上至少在 Chrome 中运行时,解释器会尝试将 `(a)` 理解为对 `f` 的调用,而此时 `g` 的结果就变成了 `f(a)`,同时由于 `f` 被调用一次,还会产生副作用,但这显然不是我们所期望的,因此我们在每一行末尾加分号来避免这种现象。
88+
89+
但并非所有问题都可以通过加分号解决,另外可能出现的一种情况是,JavaScript 允许在函数返回时不返回任何内容(即 `undefined`),如果写出这样的代码:
90+
91+
```javascript
92+
function test() {
93+
return
94+
{ a: 1 };
95+
}
96+
```
97+
98+
你可能已经见过这种写法,在返回值很复杂的时候适当换行不失为一种保持美观性的方法,然而在 JavaScript 中,由于允许返回空内容, `return` 自身又足以作为一条语句,又由于句末不需要加分号,在一些执行环境中上述代码会直接返回空值,而不会尝试解析下一行内容,这显然也不是我们所期望的。解决这一问题的一种方法是:
99+
100+
```javascript
101+
function test() {
102+
return {
103+
a: 1
104+
};
105+
}
106+
```
107+
108+
由于单个的括号不能作为语句结束,解释器会继续向下解析直到遇到分号处。
109+
110+
如果你还记得上一节中我们解构赋值一个对象时在最外层添加了小括号,这是因为在句首遇到括号时,解释器会优先认为这是一个代码块的开头,而非一个解构赋值语句,而后面的部分是不可以作为代码块理解的,从而导致执行错误:
111+
112+
```javascript
113+
let { a } = { a: 1 }; // 这里有 let 关键词,解释器不会将括号理解为代码块
114+
{ a } = { a: 2 }; // Error!
115+
({ a } = { a: 3 }); // 添加小括号后解释器将小括号内的内容视为一个(赋值)表达式,而不是代码块
116+
```
117+
118+
类似的问题在箭头函数中同样存在,我们将在下一节看到。

docs/languages/javascript/function.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const sum = function (x, y) {
2020
};
2121
```
2222

23+
这里 `sum` 作为一个变量,它的类型并非前文所提过的基本类型,因此也是一个对象,它的构造函数是 `Function`
24+
2325
函数调用的语法则和 C/C++ 语言一致:
2426

2527
```javascript
@@ -226,6 +228,42 @@ sum(1, 2); // 3
226228

227229
这也是我们强烈推荐只使用 `let, const` 关键字的原因。
228230

231+
### 箭头函数
232+
233+
一种更简洁的函数表达式的记号是这样的:
234+
235+
```javascript
236+
// 以下几种写法的结果相同,更加细节的区别在下一节
237+
const sum = (x, y) => x + y;
238+
const sum = (x, y) => {
239+
return x + y;
240+
}
241+
function sum(x, y) {
242+
return x + y;
243+
}
244+
```
245+
246+
当函数体只有一个返回语句时,可以省略外层的括号和 `return` 关键字,若只有一个参数,可以省略参数列表的小括号:
247+
248+
```javascript
249+
const plusOne = x => x + 1;
250+
```
251+
252+
由于 JavaScript 中大量存在匿名函数,上述记号将帮助你少些很多 `function` 关键字,你将在下一部分意识到这一点。
253+
254+
另外值得注意的是,尽管在只有一个返回语句是可以省略括号和 `return`,但返回一个对象字面量时会引起错误的解释:
255+
256+
```javascript
257+
// buggy
258+
const objConstructor = (value) => { key: value };
259+
```
260+
261+
这种写法会使 JavaScript 认为括号是代码段的开始,而其中的 `key: value` 是语法错误,通常我们添加小括号来告诉 TypeScript 我们要表达的是返回值字面量:
262+
263+
```javascript
264+
const objConstructor = (value) => ({ key: value });
265+
```
266+
229267
### 回调模式
230268

231269
!!! note "你 JavaScript 真正的第一步"
@@ -347,6 +385,24 @@ response.data
347385
}
348386
```
349387

388+
### 延迟执行
389+
390+
在各个 JavaScript 运行时中都包含了以下两个函数用于定时执行任务:
391+
392+
```javascript
393+
setTimeout(() => {
394+
// do something
395+
}, 1000);
396+
397+
setInterval(() => {
398+
// do something
399+
}, 1000);
400+
```
401+
402+
两个函数传入的第一个参数都是要执行的任务,第二个函数是等待的时间(毫秒数)。不同之处在于 `setTimeout` 的回调只执行一次,`setInterval` 的回调函数会以传入的时间间隔反复执行。
403+
404+
需要注意的是,由于 JavaScript 的事件循环模型,上述的延迟时间并不是精确的,例如当其他复杂任务正在阻塞运行时,任务会按照上述时间添加到队列中,但执行时间会有所延迟。
405+
350406
### 闭包
351407

352408
函数既然是一个对象,那么我们也可以用一个函数返回一个函数。比如说:
@@ -362,7 +418,9 @@ const increase5 = getIncreaser(5);
362418
increase5(10); // 15
363419
```
364420

365-
比如说上述的 `getIncreaser` 可以用于获取一个“数据增长器”,而具体给数据增加多少,则是这个函数接收的参数。`getIncreaser` 会把 `increment` 变量包装在其返回的匿名函数之中,这样就形成了一个**闭包**。闭包实际上就是一个包装了其所在环境的一些局部变量的函数。
421+
比如说上述的 `getIncreaser` 可以用于获取一个“数据增长器”,而具体给数据增加多少,则是这个函数接收的参数。`getIncreaser` 会把 `increment` 变量包装在其返回的匿名函数之中,这样就形成了一个**闭包**
422+
423+
闭包实际上就是一个函数和对其**周围状态(lexical environment,词法环境)的引用**捆绑在一起。这里要特别注意的是,闭包的所谓词法环境,指的是创建闭包时的**函数作用域**
366424

367425
但闭包会带来一定的问题,比如说这样的代码:
368426

@@ -446,3 +504,32 @@ counters[2](); // 4
446504
```
447505

448506
这样的代码不仅正确了,可读性也很好。
507+
508+
!!! note "为什么是对的?"
509+
510+
事实上这里正确并不是因为 `let` 关键字带来了什么特殊处理,而是因为作用域不再是函数作用域。
511+
考虑下面的代码,虽然使用了 `let` 关键字,但作用域仍是函数作用域:
512+
513+
```javascript
514+
function getCounters() {
515+
let arr = [];
516+
let i = 0; // 注意在这里声明的 i 是函数作用域
517+
for (i = 0; i < 3; ++i) {
518+
arr.push(function () { console.log(i * i); });
519+
}
520+
return arr;
521+
}
522+
523+
let counters = getCounters();
524+
counters[0](); // 9
525+
counters[1](); // 9
526+
counters[2](); // 9
527+
```
528+
529+
可以看到,结果仍然是错误的。
530+
531+
那么为什么作用域是局部作用域时就对了呢?
532+
原因在于在闭包捕获变量时,对局部作用域的变量会**复制**,而对函数作用域的变量会**引用**。
533+
因此,通过在 `for` 语句中使用 `let` 声明变量,我们声明了局部变量,从而获得了期望的复制捕获行为。
534+
535+
此外需要注意的是,JavaScript 在任何情况下都不会帮你复制**对象**(上面被捕获的 `int` 是基本值而非对象),如果你的闭包捕获一个对象的值,无论这个对象的作用域如何,都会产生类似问题。事实上,这里并非基本值被特殊地对待,而是对象类型的变量存储的值是到实际对象的引用,因此在捕获时复制的是引用(浅拷贝)而非实际对象(深拷贝)。除非你明确地想要保存一些值,否则在构造闭包时请注意捕获实际的值,而非引用。

0 commit comments

Comments
 (0)