pull/1034/merge
JaydonZhao 4 days ago committed by GitHub
commit 6ec242da8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,102 @@
---
name: learn-tidy
description: >
整理用户在 js_notes/ 学习练习里刚写完或改完的代码 —— 跑真实输出、把错法整理成
❌/✅ 对照痕迹、修正写错的结果注释、把有教益的新错误吸收进 my-mistakes.md。
当用户说「整理」「整理一下」「tidy」「收拾干净」「帮我整理这段/这题」,或刚做完一章练习、
刚改完一段试验代码、留下了对错混杂的多版尝试时,主动用这个 skill。它是这个学习仓库
反复要做的收尾动作,专门服务 js_notes/ 的教学约定,别用内置 code-review 代替。
---
# learn-tidy · 整理学习练习代码
把用户刚写/改的练习或试验代码"收拾干净",让它成为将来复习时**看得懂、可追溯、能跑**的
学习痕迹。这是本仓库(30-Days-Of-JavaScript)`js_notes/` 反复要做的收尾动作。
先读 `CLAUDE.md``js_notes/README.md` 里的辅导规则(它们是**具有约束力的**),本 skill
是那套规则在"整理"这个动作上的具体落地,不得与之冲突。
## 什么时候用
- 用户说「整理」「整理一下 / 这段 / 这题」「tidy」「收拾干净」
- 用户刚做完一章练习(AF 之类)、或刚改完一段 `// 试验 / 探索` 代码
- 文件里留下了**对错混杂的多版尝试**(比如 3 行 reduce、2 个同名 const)
## 核心原则(为什么这么做)
成品是**文件**,不是聊天:将来的用户只会翻文件,不会翻对话记录。所以整理的目标是让文件
**自解释**——一眼看到"错在哪、为什么错、正解是什么"。而且**绝不留能跑的坏代码**:错的要么
删、要么注释成对照,活代码必须是对的。
## 流程(按顺序做,别跳步)
### 1. 先读,再跑真实输出(绝不凭记忆)
- Read 目标文件相关段落,搞清哪些是用户的练习答案、哪些是试验、哪些是还没动的「轮到你」。
- **`node js_notes/NN-topic.js`** 拿到**真实输出**。需要只查语法时先 `node --check`
- 临时验证可写 `/tmp/*.js` 跑,但**结论必须落进正式文件**,别把只存在于 /tmp 或聊天里的
结果当交付。
- 用真实输出对照用户写的"预期注释"——**注释写错的结果要改对**(这是高频问题,例如用户
`// "修改y的name"` 但实跑是 `fn`)。
### 2. 把错法整理成 ❌ / ✅ 对照(留学习痕迹)
对用户犯过的错、或对错混杂的多版尝试:
- **错的那版注释掉**(绝不留能跑的坏代码),放在**正解上方**。
- 加一行简短 `// ❌ 原因` 说清*为什么*错;正解用 `// ✅` 标出、留作**活代码**。
- 多版尝试(如 3 行 reduce)收敛成:错的都注释成 ❌ 对照,最优的一版留活代码。
- 若某版"能救回来但不优雅",可用 `// 🔸` 标注中间方案,但活代码留最干净的那版。
风格对齐用户已在 `06-arrays.js` / `07-objects.js` / `10-functions.js` 用的写法:
```js
// ❌ ^ 在 JS 是"按位异或"不是乘方:8^2=10、和平方无关
// console.log(nums.filter(n => n % 2 === 0).map(n => n^2))
// ✅ 乘方用 **(或 n*n)
console.log(nums.filter(n => n % 2 === 0).map(n => n ** 2)) // [ 64, 100 ]
```
### 3. 决定要不要吸收进 my-mistakes.md(关键判断,别做流水账)
**不是所有错误都记。** `my-mistakes.md` 是"复习时值得回看的坑",不是错误日志。三档处理:
| 情况 | 怎么办 |
|---|---|
| 新的、有教益的坑(尤其"能跑不报错"的概念坑) | ✅ 新开一条 |
| 已有条目的重演 / 变体 | 🔸 不新开,在原条目**补一句**新情境 |
| 纯手误 / 已被现有条目覆盖 / 用户已自己改好且无新东西可学 | ❌ 不记 |
新开条目沿用现有格式:**错法 → 现象 → 正解/根源 → 教训**,并带 `📍来源`(文件 + 练习号)。
能跑不报错的坑标 ⭐,概念性推错(非手误)可标 ⭐⭐ 并注明"概念推错,非手滑"。若坑对应某个
报错信息,顺手更新末尾的「高频报错 → 秒查表」。
**拿不准值不值得记时,问用户。**(用户明确说过:记流水账他会反驳。)
### 4. 不碰用户还没做的练习
「轮到你」里用户**还没动手**的题,**绝不**往里写解答(哪怕注释掉也不行,会剧透)。整理只针对
用户**已经写了**的答案/试验。
### 5. 跑一遍验证 + 收尾
- 改完后再 `node js_notes/NN-topic.js`(或 `node --check`)确认整章能跑、输出符合预期。
- 如果这次整理正好是"一章练习全部做完",可按 `CLAUDE.md` 的「一章收尾必做清单」把 README
进度勾选也带上。
### 6. commit —— 灵活判断,别默认执行
**不要每次整理都自动 commit。** 由具体情形决定:
- 若这次整理是一个**逻辑完整的收尾**(一章做完、或一段独立讲解落定),且工作区就这些改动
→ 可以提议 commit,并给出 Angular 风格 message(`docs(js_notes): ...`),**先说清要提交什么**。
- 若还在**中途**(用户可能马上继续改)、或工作区混着其它未完成改动 → **先别 commit**,
只把整理做好、告诉用户"整理完了,要提交时说一声"。
- 拿不准时,问用户要不要提交,而不是擅自 commit。
commit 前遵守 `CLAUDE.md` 的 Git 约定:干净拆分、绝不提交私密内容、不丢弃用户工作。
## 结束时给用户的话
简短汇报:跑出来的真实输出、留了哪些 ❌/✅ 痕迹、有没有(以及为什么)动 my-mistakes、
要不要 commit。别长篇复述文件内容——用户会自己翻文件。

@ -93,7 +93,7 @@ console.log(backEnd)
["Node", "Express", "MongoDB"] ["Node", "Express", "MongoDB"]
``` ```
If we like to skip on of the values in the array we use additional comma. The comma helps to omit the value at that specific index If we like to skip one of the values in the array we use additional comma. The comma helps to omit the value at that specific index
```js ```js
const numbers = [1, 2, 3] const numbers = [1, 2, 3]
@ -184,7 +184,7 @@ Node Express MongoDB
### Destructuring Object ### Destructuring Object
When we destructure the name of the variable we use to destructure should be exactly the same as the key or property of the object. See the example below. When we destructure, the name of the variable we use to destructure should be exactly the same as the key or property of the object. See the example below.
```js ```js
const rectangle = { const rectangle = {
@ -258,7 +258,6 @@ const calculatePerimeter = rectangle => {
} }
console.log(calculatePerimeter(rect)) // 60 console.log(calculatePerimeter(rect)) // 60
//with destructuring
``` ```
```js ```js

@ -0,0 +1,169 @@
# CLAUDE.md
本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。
## 这个仓库到底是什么
这里并存着两层内容:
1. **上游教程** —— `NN_Day_*` 文件夹(例如 `01_Day_Introduction`
`23_Day_Event_listeners`),以及各语言翻译文件夹(`Korea/`、`RU/`、
`Spanish/`……)。这是 Asabeneh 的 "30 Days Of JavaScript" 课程,作为**只读**
参考资料克隆进来。**不要修改或"修复"这些文件** —— 其中的错误(例如 `main.js`
依赖另一个单独加载的 `variable.js` 里的变量)是刻意为之的教学产物,不是 bug。
2. **`js_notes/`** —— 用户自己的学习笔记,也是**活跃工作区**。
几乎所有真正的工作都发生在这里。它**按知识主题组织,而非按天**
(`01-basics`、`02-variables`、……`07-objects`……),因为这个编号反映的是
用户的学习顺序,而不是教程的天数。
用户是**正在入门 JavaScript 的初学者**(有一些 Python/Linux 背景)。
每次会话都是交互式辅导:讲一个主题、让用户练习、复审他的代码。
## `js_notes/` 的结构与约定
- **一个主题一个文件**,平铺在 `js_notes/` 根目录:`NN-topic.js`(例如
`09-loops.js`)。每个文件都自成一体,包含"学习 + 练习 + 复习",分为三个
带标签的区块,顺序如下:
1. `// ═════ 概念总结 ═════` —— 以注释形式呈现概念(含对齐的 ASCII 表格)
2. `// ═════ 示范 ═════` —— 可运行的演示代码,可带预期输出注释
3. `// ═════ 轮到你 ═════` —— 练习:只给要求,由用户自己写代码
`js_notes/README.md` 是索引 + 进度地图。
- **历史沿革:** 主题过去是 `NN-topic/` 文件夹,里面放 `notes.md` +
`practice.js`;后来被扁平化为单个 `NN-topic.js` 文件。`my-mistakes.md` 里有些
`📍来源` 链接仍指向旧的 `../NN-topic/practice.js` 路径 —— 请把它们视作
`../NN-topic.js`(只在你本来就在编辑那条条目时才顺手修链接;不要对用户的文件
做一次性的全量重写)。
- `js_notes/00-concepts/` 保持为一个**文件夹**,存放跨主题的 `.md` 笔记
(值 vs 引用、调用形式、bash-vs-js 短路、分号与 ASI),外加
**`my-mistakes.md`** —— 一份用户实际犯过的错误的滚动记录,每条采用
错法→现象→正解→教训 格式,并带一个指回原练习的 `📍来源` 反向引用。
**`js_notes/README.md` 含有具有约束力的辅导规则 —— 教学前先读它。**
其中最关键的几条:
- **不替用户写答案。** 在练习文件里,"轮到你"区块只给要求 —— 不给解答,
连注释掉的解答也不行(那会破坏练习)。提示只指方向("这两个方法可以链式
调用"),不轻易给出完整的一整行,比如 `raw.trim().toLowerCase()`
- **让用户自己修自己的代码。** 指出哪里错了、为什么错;不要替他改练习答案。
- 演示区块(教学示例)可以用注释写出预期输出;练习区块不可以。
- **当用户犯了新错误时,把它吸收进 `00-concepts/my-mistakes.md`**
(错法→现象→正解→教训 格式,带 `📍来源` 索引);或者如果不值当单开一条,就直接在那个笔记里 Edit 也行,这样复习时可追溯。
但**只记真正有教益的错误** —— 已被现有条目覆盖的纯手误,或用户已经自己
修好、没有新东西可学的错误,不值得新开一条(记流水账的话用户会反驳)。
拿不准时,问一下。
- 一个主题完成后,更新 `js_notes/README.md` 里的进度勾选框。
### 一章收尾必做清单(用户的长期要求)⭐
当一章的练习全部做完、确认无误时,**每次都要走完这套收尾流程**(别遗漏、别偷懒):
1. **跑一遍验证**:`node js_notes/NN-topic.js` 确认整章(含用户练习答案)能跑、
输出符合预期;必要时 `node --check` 先查语法。
2. **留学习痕迹**:用户练习中犯过的错,按"改用户代码时留学习痕迹"那条,在文件里
保留 `❌/✅` 对照(绝不留能跑的坏代码)。
3. **吸收错误**:本章冒出的、有教益的新错误,按上面规则并入 `my-mistakes.md`
(值当就新开条目、不值当就在相关条目补一句;纯手误/已覆盖的不记)。
4. **更新进度**:勾选 `js_notes/README.md` 里该章的进度框(`[ ]` → `[x]`),
必要时补上本章实际覆盖到的关键词。
5. **提交**:按逻辑拆成干净的 commit 提交(Angular 风格 message);工作区留干净。
6. **导向下一步**:简述下一章要学什么、和已学内容/pi 目标的关联,让用户决定是否继续。
### 改用户代码时留下学习痕迹(用户的长期要求)
每当用户让你改他的代码或加标记时,**把错误版本作为注释掉的 `❌ / ✅` 对照
保留**在修复的旁边 —— 好让未来的他看到*错在哪、为什么错*,而不仅仅是改对后的
那一行。这与用户已经在 `06-arrays.js` / `07-objects.js` 里用的风格一致:
```js
// ❌ splice 返回的是"被删除的元素",不是修改后的数组
// console.log(q.splice(1, 0, 'second'))
// ✅
q.splice(1, 0, 'second')
console.log(q)
```
这类痕迹的规则:
- 把错误行注释掉(绝不留下能跑的坏代码),放在修复的**上方**。
- 一行简短的 `// ❌ 原因` 说明*为什么*错;`// ✅` 标出正确的代码。
- 这适用于**演示/教学区块,以及用户明确要求的修复** ——
它**不**推翻"绝不写答案":对于用户还没动手的 `轮到你` 练习,仍然不要
往里丢解答。
- 由此浮现出的、有分量的新陷阱,通常也应加一条 `my-mistakes.md` 条目
(以上文"只记有教益的"这条为准)。
### 讲清*为什么*,而不只是规则(用户的长期要求)
比起干巴巴的"就是这么规定的",用户从底层机制里学得快得多。只给规则、
不给原因的简略笔记是不够的 —— 用户明确指出 `// 带 {} 就得自己写 return`
太晦涩。当一条规则有其原因时,**先讲原因**;机制一旦讲清,规则就不言自明
(而且往往好几条规则会归结到同一个原因)。
举例说明 —— 箭头函数的函数体与 `return`。**先说出那句最核心的话**,再让
一切作为它的推论自然而然地推出(能找到那句核心才是本事 —— 用户曾修正过一版
`{` 的两种身份出发、而非从那句核心出发的草稿):
- **核心一句:`=>` 的右侧期待一个*表达式***(某个求值后得到一个值的东西)。
- 给它一个表达式 → 那*就是*那个值 → JS 把它返回(隐式返回)。
在那里写 `return` 是非法的:`return` 是*语句*,不是表达式,所以
`(a, b) => return a + b``SyntaxError`
- `{ ... }` 是逃生舱:以 `{` 开头的函数体会让 JS 把它当作*语句块*,
语句块不产生值 → 你必须显式 `return`,否则得到 `undefined`
- 由这同一句核心推出:对象字面量也以 `{` 开头,于是和语句块规则冲突 →
用括号包起来以强制进入表达式上下文:`(x) => ({ name: x })`。
由此得出的实操习惯:
- 优先说"X **因为**解析器/引擎看到了 Y",而不是"总是这样做 X"。
- **用运行代码来验证**,当它能让论点更锋利时 —— 用户看重看到真实的报错
信息或输出,胜过一句断言(例如用 `node --check` 显示一个真实的
`SyntaxError`)。
### 讲解【直接写进文件】,别只留在聊天里(用户的长期要求)⭐
用户反馈:讲解如果只写在聊天回复里,事后几乎不会再翻出来;而临时验证文件
(`/tmp/*.js`)他更是看不到。所以**成品是文件,不是聊天**。当一段讲解值得
留存时:
- **直接写进对应的 `NN-topic.js`**(概念区讲原理、示范区放可跑代码),
而不是"先在聊天里长篇讲一遍、再写一遍文件" —— 那样既浪费上下文,产物
用户又拿不到。聊天里只做**极简导读 + 指向文件哪一节**。
- 临时用 `/tmp/*.js` 跑验证没问题,但**结论要落进文件**;别把只存在于
`/tmp` 或聊天里的解释当成交付。
- 跨主题的点,写进该去的那一章,并在相关章节挂一句呼应(例:提升 hoisting
写进 10-functions.js,在 02-variables.js 的 var 那格挂一句指过去)。
- 这条和上面"讲清为什么""留 ❌/✅ 痕迹"是一套:原理讲清 + 落进文件 +
可追溯,才是一次完整的讲解。
## 运行代码
这里**没有构建系统、没有 package.json、没有 linter、没有测试套件** ——
这是一个学习仓库,不是一个应用。代码逐个文件运行:
```bash
node js_notes/06-arrays.js # 运行单个主题文件
node --check js_notes/09-loops.js # 只做语法检查,不执行
```
在编辑器里,用户也会用 **Quokka.js**(内联显示实时 `console.log` 值;免费版 ——
不支持 `//?` 实时注释)或 **Code Runner**(▷ 按钮)来运行文件。
**自足文件 vs 浏览器文件:** 一个 `.js` 文件如果自己定义了它用到的一切、
且不碰 `document`,就能在 Node/Quokka/Code Runner 下运行。而依赖另一个脚本里
变量的文件,或使用 `document`/DOM 的文件(第 21 天以后以及那些小项目),
必须通过它们的 `index.html` 在浏览器里打开、配合 F12 控制台 —— 在 Node 下
运行会抛错(例如 `firstName is not defined`、`document is not defined`)。
## 内容语言
散文、笔记、讲解用**中文**;代码、标识符、commit message 用英文。
Commit message 遵循 Angular 风格(`<type>(scope): description`)。
## Git 远端
这个克隆是从原课程 fork 出来的:
- `origin``github.com/JaydonZhao/30-Days-Of-JavaScript`(用户的 fork;**推到这里**)
- `upstream``github.com/Asabeneh/30-Days-Of-JavaScript`(原作者;拉取教程更新用
`git fetch upstream && git merge upstream/master` —— 用 merge,不用 rebase)
工作直接发生在 `master` 上(这个 fork 的 `master` 独立于 upstream 的,所以
用户的提交永远不会碰到别人的仓库)。这个 fork 是**公开的** —— 不要提交任何
私密内容。

@ -0,0 +1,67 @@
# 通用概念 · 三种调用形式:`值.方法()` vs `函数()` vs `类.方法()`
> 跨主题概念,不属于某一天。困惑"什么时候用点、什么时候传参"时看这篇。
## 核心问题:这个函数"属于"谁?
### 形式一:`值.方法()` —— 方法(method)
函数**属于某个值/对象**,是它"自带的能力",用点 `.` 调用。
**点前面那个值 = 被操作的主角**,不用再传进去。
```js
'hello'.toUpperCase() // toUpperCase 是字符串自带的能力
;[1, 2, 3].map((x) => x * 2) // map 是数组自带的能力
'a,b'.split(',') // split 属于字符串
```
### 形式二:`函数(值)` —— 独立函数(function)
函数**不属于任何特定值**,是自由的工具,数据当参数**传进括号**。
```js
Number('42') // 转谁就传谁进去
parseInt('42px')
isNaN(x)
```
### 形式三:`类.方法(值)` —— 静态方法(static method)
函数挂在某个**类/工具箱**下做归类,数据仍然**传进去**。
```js
Array.isArray([1, 2, 3]) // isArray 挂在 Array 名下
Math.max(1, 2, 3) // max 挂在 Math 名下
Number.isNaN(x) // 挂在 Number 名下
Object.keys(obj) // 挂在 Object 名下
```
> ⚠️ 区分:`Array.isArray(arr)` 的 `Array`(大写=类/工具箱)和 `[1,2,3].map()` 点前的
> 数组实例(具体的值)是两回事。**大写开头 = 工具箱;点前是具体值 = 实例。**
## 对照表
| 形式 | 例子 | 数据在哪 | 感觉 |
|---|---|---|---|
| `值.方法()` | `'hi'.toUpperCase()` | **点前面** | 值自带的能力 |
| `函数(值)` | `Number('42')` | **括号里** | 自由工具 |
| `类.方法(值)` | `Array.isArray(x)` | **括号里** | 工具箱里的工具 |
## "什么时候用哪个"?
**真相:不是你选的,是语言设计时定死的。** 每个功能造出来就固定了形式,只能记/查。
- 转大写只能 `str.toUpperCase()`,不能 `toUpperCase(str)`
- 判断数组只能 `Array.isArray(x)`,不能 `x.isArray()`
但有规律可猜:
1. **操作字符串/数组/对象内容的** → 大多 `值.方法()`
2. **类型转换/数学/解析类工具** → 大多 `函数()``类.方法()`(`Number`、`Math.`、`JSON.parse`)
3. **`Math.` / `JSON.` / `Object.` / `Array.` 开头** → 几乎都是静态方法,数据传进去
## 记忆诀窍
> 问:**"这是某个值天生就会的,还是外部工具对它做的?"**
> - 天生就会(大写化、截取、拆分)→ `值.方法()`
> - 外部工具处理它(转类型、算最值、解析)→ `函数(值)` / `工具箱.方法(值)`
## 🔧 实用技巧
**不确定时,在编辑器里敲 `值.` 看自动补全弹什么** —— 弹出来的就是它自带的方法;
没弹出来的,就得找独立函数或静态方法。让 VS Code 帮你查最快。

@ -0,0 +1,53 @@
# 通用概念 · 表达式(expression) vs 语句(statement)⭐
> 跨主题核心区分。它决定"这东西能不能当值用",串起箭头函数、return、三元、if。
> 相关:`../10-functions.js` §2(箭头 => 期待表达式)、`../05-conditionals.js`(三元 vs if)。
## 一句话分界
- **表达式 = 能算出一个【值】的东西** → 有值,能赋给变量、能当参数传。
- **语句 = 执行一个【动作】的东西** → 在你的代码里【没有值】,不能当值用。
判断诀窍:**能不能塞进 `=` 右边(或当函数参数)?能 = 表达式;塞进去报 SyntaxError = 语句。**
## 谁是表达式,谁是语句
| 表达式(有值) | 语句(没值) |
|---|---|
| `1 + 2`、`n * 2` | `if (...) {...}` |
| `'hi'`、`` `${x}` `` | `for / while (...) {...}` |
| `x = 99`(赋值也是表达式!值是 99) | `let x = 1` / `const y = 2`(声明) |
| `5 > 3 ? 'a' : 'b'`(三元) | `return x` / `break` / `continue` |
| `fn(1, 2)`(函数调用) | `function f(){}`(函数声明) |
```js
const a = 1 + 2 // ✅ 1+2 是表达式,有值 3
const b = if (true) { 1 } // ❌ SyntaxError:if 是语句,没有值,不能赋
const m = 5 > 3 ? 'a' : 'b' // ✅ 三元是表达式;换成 if 就不行 → 这就是三元存在的意义
```
## ⚠️ 最容易被骗的地方:控制台里"语句好像有值"
在浏览器 F12 控制台 / `eval` 里敲一句 `if (true) { 5 }`,回车会显示 `5`,看着"像有返回值":
```js
eval('if (true) { 5 }') // 5 ← 底层的"完成值(completion value)"
eval('let z = 1') // undefined
const bad = if (true) {5} // ❌ SyntaxError:想把它捞进变量,立刻现原形
```
真相:语句在**引擎底层**确实有个"完成值",但**你在正常代码里拿不到**,只有
`eval` 和 REPL(控制台)会把它显示出来。**别被控制台的显示骗成"语句能当值用"。**
## 为什么这个区分重要(它串起好几个点)⭐
- **箭头函数**:`=>` 右边【期待表达式】。所以 `(a,b) => a+b` ✅(a+b 是表达式);
`(a,b) => return a+b` ❌ SyntaxError —— 因为 `return` 是【语句】,没有值,不能放这。
- **三元 vs if**:三元是表达式(能 `const x = 条件 ? a : b`),if 是语句(不能赋值)。
需要"按条件得到一个值"时用三元,正因为它有值。
- **隐式返回**:箭头后给表达式 → 它就是值 → 自动返回;给 `{语句块}` → 块没值 → 得手写 return。
## ✅ 一句话记死
> **表达式有值、能当值用(赋值/传参);语句是动作、在代码里没值(塞进 `=` 右边就 SyntaxError)。**
> 控制台里语句"显示出一个值"是 REPL 在显示"完成值",别当真。

@ -0,0 +1,201 @@
# 🕳️ 我的踩坑记录
> 这里记录我**自己真实犯过**的错(不是泛泛知识点)。复习时先扫这里 —— 我最容易在这些地方翻车。
> 每条格式:**错法 → 现象 → 正解 → 教训**,并标注**📍来源**(点开可回到原题)。
---
## A. 方法忘加括号
📍来源:`../04-strings/practice.js` 练习 A(`../04-strings/notes.md` §1)
```js
city.toUpperCase // ❌ 打印出 [Function: toUpperCase],不是结果
city.toUpperCase() // ✅ 加 () 才执行
```
- **现象**:打印出一堆 `[Function: xxx]` 而不是想要的值
- **教训**:**方法是"动作",必须加 `()`**;`.length` 是属性不加括号。分不清就问:这是"数据"还是"动作"?
## B. 模板字符串用错符号
📍来源:`../02-variables/practice.js` 第 6 组(`../01-basics/notes.md` §4)
```js
`我叫 &{myName}` // ❌ 用了 &,原样打印 "我叫 &{myName}"
`我叫 ${myName}` // ✅ 是 $ 不是 &
```
- **教训**:模板插值是 **`$` + `{}`**,不是 `&`。且只在反引号 `` ` `` 里生效。
## C. 方法名拼写错
📍来源:`../04-strings/practice.js` 练习 B
```js
email.spilt('@') // ❌ 报错:email.spilt is not a function
email.split('@') // ✅
```
- **现象**:`X is not a function`
- **教训**:看到 `is not a function`,先检查**拼写**。
## D. "能算 ≠ 能看见"(反复犯)
📍来源:`../04-strings/practice.js` 练习 E、`../06-arrays/practice.js` 练习 E(`../01-basics/notes.md` §5)
```js
array.join('-') // ❌ 算出来了但没打印,屏幕啥都没有
console.log(array.join('-')) // ✅
```
- **教训**:文件里**必须 `console.log`** 才看得到结果。裸表达式会算但不显示。
## E. 方法返回值没接住(反复犯)⭐
📍来源:`../04-strings/practice.js` 练习 D/E、`../06-arrays/practice.js` 练习 E
```js
sentence.split(' ') // ❌ 拆了但没接住,等于白拆
const words = sentence.split(' ') // ✅ 用变量接住
```
- **教训**:`slice / split / join / map / toUpperCase...` 都**返回新值、不改原**,必须用变量接住或直接打印。
## F. 改原 vs 返回新,分不清 ⭐⭐
📍来源:`../06-arrays/practice.js` 练习 D/E(`../06-arrays/notes.md` §4-5)
- **改原数组**:`push / pop / shift / unshift / splice`
- **返回新值(不改原)**:`slice / split / join / map / filter`
- `splice` 尤其坑:**返回的是"被删元素",不是修改后的数组** → 插入后要打印**原数组**,别打印返回值
```js
q.splice(1, 0, 'x') // 改的是 q 本身
console.log(q) // ✅ 打印原数组
console.log(q.splice(...)) // ❌ 打印的是被删元素([])
```
## G. slice 用 length-1 丢了最后一个字符
📍来源:`../04-strings/practice.js` 练习 D(`../04-strings/notes.md` §4)
```js
name.slice(1, name.length - 1) // ❌ -1 少取一位
name.slice(1) // ✅ 省略 end 取到结尾
```
- **教训**:`slice` **不含 end**;要取到结尾直接省略第二个参数。
## H. 变量没声明就赋值
📍来源:`../06-arrays/practice.js` 练习 E(`../02-variables/notes.md` §2-3)
```js
array = sentence.split(' ') // ❌ 裸奔,泄漏成全局变量(严格模式报错)
const array = sentence.split(' ') // ✅ 必须 const/let 声明
```
- **教训**:变量**一定先声明**。
## I. 引号不配对
📍来源:`../07-objects/practice.js` 练习 A(`../01-basics/notes.md` §4)
```js
title: "JavaScript' // ❌ 双开单关,整个文件从这里崩
title: "JavaScript" // ✅ 开关同一种
```
- **现象**:从这行起后面全报错
- **教训**:引号**开和关必须同种**。
## J. 变量名定义和使用对不上
📍来源:`../07-objects/practice.js` 练习 C
```js
const cat = {...}
Object.values(car) // ❌ car is not defined(定义 cat 用 car)
```
- **现象**:`X is not defined`
- **教训**:看到 `is not defined`,检查**名字是否一致**(typo)。
## K. 下标从 0 开始,越界读 undefined ⭐
📍来源:`../07-objects/practice.js` 练习 E(`../06-arrays/notes.md` §1)
```js
// 3 个元素:people[0] people[1] people[2]
people[3].name // ❌ 越界:Cannot read properties of undefined
```
- **教训**:下标**从 0 开始**,"第 N 个" = `[N-1]`;最后一个 = `[length-1]`
## L. 对象方法用了箭头函数,this 失效 ⭐⭐
📍来源:`../07-objects/practice.js` 练习 D(`../07-objects/notes.md` §7)
```js
describe: () => this.name // ❌ 箭头函数没有自己的 this → undefined
describe: function () { return this.name } // ✅ 用 function
describe: function () => {...} // ❌ 语法错:function 和 => 不能同时写
```
- **教训**:**对象方法用 `function`,不用箭头 `=>`**(箭头没有自己的 this)。两种写法只能选一种。
## M. const 同名声明两次
📍来源:`../07-objects/practice.js` 练习 D;又见 `../10-functions.js` 练习 F
```js
const student = {...}
const student = {...} // ❌ Identifier 'student' has already been declared
```
- **教训**:同作用域 `const` 不能重名;想留"错误对照"就**注释掉**旧的,别当活代码。
- **重演情境(10章F)**:迭代改进时【新写了一版更好的 `const repeat`,却忘了删/注释掉旧版】
→ 两个活的 `const repeat` → 整个文件直接报错跑不了。
改进代码时:**旧版要么删,要么注释成 ❌ 对照,绝不能两个都当活代码**。
## N. 以为"代码块能算出值、会自动返回"⭐⭐(概念推错,非手滑)
📍来源:`../10-functions.js` 概念区第 2 节(箭头函数)
```js
const add = (a, b) => { a + b } // ❌ 以为块算出 a+b 就会返回 → 实际返回 undefined
const add = (a, b) => a + b // ✅ 无 {}:右边是表达式(值),自动返回
const add = (a, b) => { return a + b } // ✅ 有 {}:块里必须手写 return
```
- **现象**:函数默默返回 `undefined`,**不报错**(最难查)
- **根源**:核心一句 —— **`=>` 右边期望一个「表达式」(能算出值的东西)**。
- 表达式(`a+b`)=【有值】→ 直接返回;能赋值 `const x = a+b`
- 代码块(`{...}`)=【一堆动作,不产值】→ 不能赋值 `const x = {a+b}` ❌ 报错
- 所以 `{ a+b }` 里那个 `a+b` 被算出来**却被丢弃**,函数无 return → undefined
- **教训**:别把"代码块"当"能出值的表达式"。**块不产值**,所以进了 `{}` 就必须自己 `return`
## O. 箭头函数返回对象忘加括号 ⭐
📍来源:`../10-functions.js` 概念区第 2 节
```js
const make = (name) => { name: name } // ❌ { 被当代码块,返回 undefined(不报错!)
const make = (name) => ({ name: name }) // ✅ () 逼它当表达式 → 返回对象
```
- **现象**:想返回对象,却得到 `undefined`,且**不报错**
- **根源**:对象字面量和代码块都以 `{` 开头,`=> {` 会被优先当**代码块**;
里面的 `name:` 又被当成**标签语句**(JS 给循环起名字的老语法,`名字:`),于是静默返回 undefined
- **教训**:箭头函数**直接返回对象**,用 `()` 把对象裹起来,逼回"表达式"语境。
## P. 用 `^` 当乘方(其实是按位异或)⭐(能跑不报错)
📍来源:`../11-higher-order-functions.js` 练习 E
```js
8 ** 2 // 64 ✅ 乘方用 **(或 n*n)
8 ^ 2 // 10 ❌ ^ 是"按位异或(XOR)",不是乘方!8^2=10、10^2=8
```
- **现象**:算出个莫名其妙的数,**不报错**(最难发现),和平方毫无关系
- **教训**:JS 的乘方是 `**`;`^` 是位运算(异或)。从数学/别的语境迁移来的人常栽。
## Q. `arr[a, b]` 当成切片(其实是逗号运算符,取单个)⭐
📍来源:`../11-higher-order-functions.js` 练习 F
```js
s[1, s.length-1] // ❌ 逗号运算符只取最后一个值 → 等于 s[s.length-1],取【单个字符】
s.slice(1) // ✅ 切片要用 .slice(见 04 章)
```
- **现象**:想切一段,却只得到一个字符
- **教训**:JS 没有 `[start, end]` 切片语法;`[]` 里的逗号是"逗号运算符"。
逗号运算符 `a, b`:**从左到右全都算一遍,但整个表达式的值 = 最后一个**
(`(1,2,3)` 的值是 `3`)。所以 `s[1, len-1]` = `s[len-1]`,取单个字符。
切字符串/数组一律用 `.slice(start, end)`。日常几乎用不到逗号运算符,认得它别踩坑即可。
---
## 🔁 高频报错 → 秒查表
| 报错信息 | 最可能的原因 | 看哪条 |
|---|---|---|
| `X is not a function` | 方法名拼错 | C |
| `X is not defined` | 变量名 typo / 没声明 | H、J |
| `Cannot read properties of undefined` | 下标越界 / 键不存在 | K |
| `has already been declared` | const 重名 | M |
| 打印出 `[Function: xxx]` | 方法忘加 `()` | A |
| 屏幕啥都没有 | 忘了 console.log / 没接住返回值 | D、E |
| 从某行起全报错 | 引号没配对 | I |
| 函数返回 `undefined`(不报错) | `{}` 里忘 return / 箭头返回对象没加 `()` | N、O |
| 算出莫名其妙的数(不报错) | `^` 当乘方(实为异或)/ `[a,b]` 当切片 | P、Q |

@ -0,0 +1,81 @@
# 通用概念 · 分号 `;`:什么时候【必须】写
> JS/TS 每句结尾的 `;` 绝大多数可省(引擎会自动补),但**有几种情况不写就是坑**。
> 本篇只记"**必须写、不写就出 bug**"的场景。相关:`../09-loops.js`(无分号风格)。
## 背景:ASI —— 引擎会自动补分号
JS 有个机制叫 **ASI(Automatic Semicolon Insertion,自动分号插入)**:
你不写 `;`,引擎在换行处**通常**会替你补上。所以这两种写法行为完全一样:
```js
const a = 1; // 写了
const a = 1 // 没写,ASI 自动补 —— 等价
```
> 本仓库 `js_notes/` 统一用**无分号**风格(Prettier 默认),保持一致即可,别混着来。
> 但无分号的前提是:**知道下面这几个 ASI 会猜错的地方,主动兜底。**
## ⚠️ 坑 1:下一行以 `(``[` 开头 → 必须在【行首】补 `;`
ASI 看到下一行还能和上一行"连成一句",就**不补**分号,于是两行被黏成一句:
```js
// 💥 出错:ASI 不补,被理解成 5[1,2,3]... 或 b(...)
const x = 5
[1, 2, 3].forEach((n) => console.log(n)) // TypeError: Cannot read properties...
const b = foo
(function () {})() // foo(function(){})() —— 把 foo 当函数调用
```
**正解:在以 `(` / `[` 开头的那行,行首加 `;`**(不是上一行结尾):
```js
const x = 5
;[1, 2, 3].forEach((n) => console.log(n)) // ✅ 行首 ; 把两句切开
```
口诀:**一行别以 `(``[` 开头;非要这么写,行首先放一个 `;`。**
(常见于:上一行是赋值/表达式,下一行想直接 `[...]`.map() 或 `(...)` 立即执行。)
## ⚠️ 坑 2:`return` / `throw` 等后面【不能】换行
这几个关键字后面一旦换行,ASI 会**强制**在关键字后补 `;`,把真正想返回的东西丢掉:
```js
// 💥 等于 return; 换行后的对象被当成"死代码"丢弃 → 函数返回 undefined
function make() {
return
{ name: 'Tom' }
}
make() // undefined,不是那个对象!
```
**正解:要返回的东西和 `return` 写在同一行**(对象太长就用括号包起来接着写):
```js
function make() {
return { name: 'Tom' } // ✅ 同一行
}
```
同样受影响的:`return` / `throw` / `break` / `continue` / `yield` —— **后面别换行**。
## ⚠️ 坑 3(顺带):同一行写两句,中间【必须】有 `;`
分号真正的本职工作是"分隔语句"。想把两句挤在一行,中间**必须**加 `;`:
```js
let a = 1 let b = 2 // 💥 语法错误
let a = 1; let b = 2 // ✅ 一行两句,靠 ; 分隔
```
## ✅ 一句话
> 平时跟着仓库的**无分号**风格写就行,ASI 帮你兜底。只死记两条保命规则:
> 1. **行首是 `(``[`** → 这一行**行首**补 `;`;
> 2. **`return`/`throw`/`break`/`continue` 后面不换行**(要返回的写同一行)。
>
> 装了 Prettier 的话,保存时会自动帮你处理坑 1 的行首 `;`,更省心。
> TS 规则完全一样(TS 是 JS 超集),只是 TS 项目社区**更偏爱每句都写 `;`** —— 那是团队约定,不是语法强制。

@ -0,0 +1,79 @@
# 通用概念 · `&&` / `||`:Linux(bash) vs JavaScript
> 跨环境对照。shell 和 JS 来回切时看这篇。相关:`../05-conditionals/notes.md` 第 5 节。
## 相同点:都是"短路求值"
"左边决定要不要看右边"—— 这个思路两边完全一致。
```bash
# bash
命令A && 命令B # A 成功才执行 B
命令A || 命令B # A 失败才执行 B
```
```js
// JS
a && b // a 为真才看 b
a || b // a 为假才看 b
```
## 不同点:操作数是"命令" vs "值"
| | Linux (bash) | JavaScript |
|---|---|---|
| 左右两边是 | **命令**(程序) | **值 / 表达式** |
| "真/假"看什么 | **退出码**:`0`=成功=真,非0=失败=假 | **truthy/falsy**:值本身 |
| 返回什么 | 最后执行命令的退出码 | **操作数原值** |
## ⚠️ 最大的坑:`0` 的真假正好相反
```bash
# Linux:退出码 0 = 成功 = 真
true && echo yes # 打印 yes(true 的退出码是 0)
```
```js
// JS:数字 0 = falsy = 假
0 && console.log('yes') // 不打印(0 是假)
```
- Linux 的 `0`:退出码 0 = "没出错" = **真**
- JS 的 `0`:数值零 = "空/无" = **假**
**同一个 `0`,两个世界真假颠倒。** 这是切换时最容易栽的地方。
## 相同的惯用法(思路可直接迁移)
```bash
# bash:|| 兜底
result=$(cat file) || result="default"
# bash:&& 前置守卫
[ -f config ] && load_config
```
```js
// JS:|| 设默认值
const result = readFile() || 'default'
// JS:&& 守卫
config && loadConfig()
```
## ⚠️ 另一个差异:两边"能串什么"
bash 的 `&&`/`||` 串的是**命令**(任意命令都行,`mkdir dir && cd dir`)。
JS 的 `&&`/`||` 是**运算符**,两边只收**表达式**(有值的东西),不能塞【语句】:
```js
user && user.login() // ✅ 函数调用是表达式,行(守卫)
cache || compute() // ✅ 兜底
true && const x = 1 // ❌ SyntaxError:const 声明是【语句】
true && if (a) {...} // ❌ SyntaxError:if 是【语句】
```
→ 所以【不能】像 shell 那样用 `&&` 串任意语句块。要按条件跑一段带 if/for/多行的逻辑,
就老老实实用 `if`;`&&`/`||` 只适合串"一个简短表达式"(一次函数调用、一个赋值)。
(为什么?见 `expression-vs-statement.md`:运算符两边只接表达式。)
## ✅ 一句话
> 短路逻辑 Linux 和 JS **一致**,惯用法也像(`||`兜底、`&&`守卫)。
> 区别:**bash 操作"命令"看退出码,JS 操作"值"看 truthy/falsy**;
> 最大的坑:**`0` 在 bash 是真(成功),在 JS 是假**。

@ -0,0 +1,82 @@
# 通用概念 · 值 vs 引用(value vs reference)⭐⭐
> 跨主题核心概念。函数传参、对象、数组章都会反复用到。JS 最重要的底层认知之一。
## 一、两大阵营
| | 原始值(primitive) | 引用类型(reference) |
|---|---|---|
| 有哪些 | `number` `string` `boolean` `undefined` `null` `bigint` `symbol` | `object` `array` `function` |
| 变量里存的 | **值本身** | 指向对象的**地址(引用)** |
| 赋值给别人时 | **复制一份副本** | **共享同一个对象** |
| 能原地改内部吗 | ❌ 不可变 | ✅ 可以 |
**一句话:原始值"复制",引用类型"共享"。**
## 二、决定性证据:改一个,另一个变不变?
### 原始值 → 复制副本,各改各的
```js
let a = 'hello'
let b = a // b 拿到的是【副本】
a = 'world' // 改 a
console.log(b) // 'hello' ← b 没变!
```
### 引用类型 → 共享同一个,牵一发动全身
```js
let x = [1, 2, 3]
let y = x // x 和 y 指向【同一个】数组
x[0] = 99 // 改 x
console.log(y) // [99, 2, 3] ← y 跟着变了!
```
> 这就是分水岭:值类型复制、互不影响;引用类型共享、一改都变。
## 三、字符串不可变:`word[0] = 'Y'` 会怎样
字符串是**原始值**,不是对象,没有"内部格子供你寻址修改"。
```js
let word = 'hello'
word[0] = 'Y' // 静默无效(严格模式下报错)
console.log(word) // 'hello' ← 没变!
```
- **能读不能写**:`word[0]` 读字符 OK,但赋值无效
- `word[0]` 读出来的是**复制的值** `'h'`,和 word 内部已无关系 —— 不通向本体
- 严格模式(`'use strict'`)下会报 `TypeError: Cannot assign to read only property`
**想"改"字符串 → 造个新的再赋值回去:**
```js
let word = 'hello'
word = 'Y' + word.slice(1) // 'Y' + 'ello'
console.log(word) // 'Yello'
```
(这正是 04-strings D 题"首字母大写"的原理:新字符 + 剩余,再赋值)
## 四、对比:数组可以原地改
长得像(都用 `[0]`),行为相反:
```js
let arr = [1, 2, 3]
arr[0] = 99 // ✅ 数组是引用类型 → 通向对象内部 → 改得动
console.log(arr) // [99, 2, 3]
let str = 'abc'
str[0] = 'X' // ❌ 字符串不可变 → 无效
console.log(str) // 'abc'
```
## 五、为什么这很重要(伏笔)
- **函数传参**:传原始值 = 传副本(函数内改不动外面);传对象 = 传引用(函数内能改到外面的对象)
- **复制对象/数组**:`let b = a` 只是共享,不是真复制;要真复制得用别的手段(后面学)
- 很多"我明明没改它怎么变了"的 bug,根源都是引用共享
---
## ✅ 一句话记死
> **原始值(含字符串)= 复制的值,不可变,`[0]` 只能读;引用类型(对象/数组)= 共享的地址,可原地改,`[0]` 通向本体。**

@ -0,0 +1,82 @@
// 01 · 基础
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka(Cmd+Shift+P → Quokka: Start on Current File)/ 右上角 ▷ / node 本文件
// ═════ 概念总结 ═════
//
// 1. console.log() —— 打印输出
// 调试、看结果全靠它:console.log('Hello, World!')
// console —— JS 内置的"控制台"对象
// .log(...) —— 它的方法,作用是打印
// 'Hello' —— 传给它的内容
// 可一次打印多个,用逗号隔开:console.log('a =', 1)
//
// 2. 注释 //
// // 后面的内容是给人看的说明,不会被执行。删掉 // 才会"激活"那行代码。
//
// 3. 表达式:先算再用 ⭐
// 引号【外】的算式会先被算成结果,再拿去用(几乎所有语言通用):
// console.log('1 + 1 =', 1 + 1) // 1 + 1 = 2 ← 这个 1+1 会先变成 2
// - 引号【里】 '1 + 1' → 文字,原样打印
// - 引号【外】 1 + 1 → 运算,先算成 2
//
// 4. 字符串引号:单 / 双 / 反 ⭐
// '单引号' → string
// "双引号" → string,和单引号【完全等价】
// `反引号` → 模板字符串,有特殊能力
// - 单引号 = 双引号,功能没区别;社区默认用单引号(Prettier 也统一成单引号)
// - 文字里含引号时,用另一种包起来防"打架":
// "It's a nice day" ✅ 文字有 ' → 用 " 包
// 'It's a nice day' ❌ 报错:中间的 ' 提前结束了字符串
// - 反引号最有用:能用 ${变量} 直接把变量嵌进字符串,还能换行:
// const name = '小明'; console.log(`我叫${name}`) // 我叫小明
// 注意:${} 只在反引号里生效,单双引号里写它只是普通文字。
//
// 5. 怎么运行一个 .js 文件 ⭐
// ┌────────────┬──────────────────────────────────────┬──────────────────────────────┐
// │ 文件类型 │ 特征 │ 怎么跑 │
// ├────────────┼──────────────────────────────────────┼──────────────────────────────┤
// │ 自包含 │ 自己定义自己用,无 document │ ▷ / Quokka / node 文件.js ✅ │
// │ 浏览器示例 │ 靠 index.html 串多脚本共享变量, │ 浏览器 + F12 Console ✅ │
// │ │ 或含 document │ │
// └────────────┴──────────────────────────────────────┴──────────────────────────────┘
// 教程第 1 天的 main.js 报 "firstName is not defined",就是因为它依赖 variable.js,
// 属于"浏览器示例",不能用 ▷/node 单独跑。这不是 bug,是运行方式用错了。
//
// ✅ 本主题要点回顾
// 1. console.log() 打印
// 2. // 注释,不执行
// 3. 表达式先算再用(1+1→2)
// 4. 单引号=双引号(推荐单引号);反引号能嵌 ${变量}
// 5. 自包含文件才能用 ▷/Quokka 跑
// ═════ 示范 ═════
// ── 1. console.log 打印 ──────────────────────────
console.log('Hello, World!') // Hello, World!
// ── 2. 一次打印多个,用逗号隔开 ────────────────────
console.log('1 + 1 =', 1 + 1) // 1 + 1 = 2
// ── 3. 表达式先算再用:引号内 vs 引号外 ⭐ ──────────
console.log('10 * 5') // 10 * 5 ← 字符串,原样(不算)
console.log(10 * 5) // 50 ← 运算,算了
console.log('10 * 5 =', 10 * 5) // 10 * 5 = 50
// ── 4. 单引号 = 双引号 ────────────────────────────
console.log('我是单引号') // 我是单引号
console.log("我是双引号") // 我是双引号
// 文字里含撇号 → 用双引号包,避免"打架"
console.log("It's a nice day") // It's a nice day
// ── 5. 反引号:用 ${} 嵌变量 ⭐ ────────────────────
const name = '小明'
const age = 18
console.log('老办法:' + '我叫' + name + ',今年' + age + '岁') // 老办法:我叫小明,今年18岁
console.log(`新办法:我叫${name},今年${age}`) // 新办法:我叫小明,今年18岁
// ═════ 轮到你 ═════
// ── 6. 👇 轮到你:自己写一行,打印 (100 - 20) / 4 的结果 ──
console.log('结果 =', (100 - 20) / 4)

@ -0,0 +1,198 @@
// 02 · 变量与数据类型
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区每行后注释是【预期输出】,跑完自己对答案
// ═════ 概念总结 ═════
//
// 1. 什么是变量
// 变量就是给数据起个名字,方便反复使用和修改。
// let age = 18 // 把 18 存进名叫 age 的盒子
// age = 19 // 改成 19
//
// 2. let / const / ~~var~~ ⭐
// ┌────────┬──────────┬──────────────────────────────────┐
// │ 关键字 │ 能改值吗 │ 什么时候用 │
// ├────────┼──────────┼──────────────────────────────────┤
// │ const │ ❌ 不能 │ 默认首选,值不变的都用它 │
// │ let │ ✅ 能改 │ 需要改变的值(计数器、累加等) │
// │ var │ ✅ 能改 │ ⚠️ 老写法,有坑,别用 │
// └────────┴──────────┴──────────────────────────────────┘
// 原则:优先 const,确实要改再换 let。防手滑改错,代码也更好读。
// const PI = 3.14 // 常量,不会变
// let count = 0 // 会变的用 let;count = count + 1 ✅
// // PI = 3.15 // ❌ 报错:const 不能重新赋值
// ⚠️ var 为啥"有坑"?一个典型:变量声明前就用它,var 会【静默给 undefined】
// 把 bug 藏起来;而 const/let 会【当场报错】(TDZ),让你一眼发现。
// (详见 10-functions.js "提升 hoisting" 一节)
//
// 3. 变量命名规则 ⭐
// 必须遵守(否则报错):
// - 只能用字母、数字、_、$;不能以数字开头
// - 不能用关键字(如 let、const)
// - 区分大小写(age 和 Age 是两个变量)
// 社区惯例(建议遵守):
// - 用 camelCase 小驼峰:firstName、isMarried、myAge
// - 名字要有意义:age 好过 a
//
// 4. 八种数据类型
// 原始类型(Primitive)—— 7 种,最常用前 3 个:
// ┌───────────┬──────────────────┬──────────────────────────┐
// │ 类型 │ 例子 │ 说明 │
// ├───────────┼──────────────────┼──────────────────────────┤
// │ number │ 18, 3.14, -5 │ 数字(整数小数不分家) │
// │ string │ 'hi', "abc" │ 字符串 │
// │ boolean │ true, false │ 真/假 │
// │ undefined │ undefined │ 声明了但没赋值 │
// │ null │ null │ 手动表示"空" │
// │ bigint │ 10n │ 超大整数(少见) │
// │ symbol │ Symbol() │ 唯一标识(少见) │
// └───────────┴──────────────────┴──────────────────────────┘
// 引用类型(非原始):object —— {}、[]、函数等(后面专门讲)
//
// 5. typeof —— 查一个值是什么类型 ⭐
// typeof 18 // 'number'
// typeof 'hi' // 'string'
// typeof true // 'boolean'
// typeof undefined // 'undefined'
// 小坑:typeof null 会返回 'object',这是 JS 历史遗留 bug,记住即可。
//
// ── 三个最爱混的值,typeof 都出人意料(速查)⭐⭐ ──
// ┌───────────┬──────────────┬────────────────────────────────────┐
// │ 值 │ typeof 结果 │ 一句话 + 坑 │
// ├───────────┼──────────────┼────────────────────────────────────┤
// │ undefined │ 'undefined' │ 系统给的"还没值";自成一个类型 │
// │ null │ 'object' ⚠️ │ 你给的"空";typeof 说它 object 是坑 │
// │ NaN │ 'number' ⚠️ │ 无效数字,属 number;NaN!==NaN 是坑 │
// └───────────┴──────────────┴────────────────────────────────────┘
// 记:null 不是真 object(历史 bug);NaN 不是"不是数字",它就是 number。
// 详见下方 §6(undefined vs null)、§7(NaN)。
//
// 6. undefined vs null ⭐
// 两个都表示"没有",区别在【谁放的、是不是有意】:
// - undefined:系统给的"还没值"(默认状态)。像杯子你还没倒过。
// - null:你主动给的"这里【有意】空着"(刻意清空/占位)。像杯子你特意倒空并标记。
// let x; console.log(x) // undefined ← 没赋值,系统默认
// let y = null // null ← 你主动清空
// null === undefined // false ← 概念不同:一个"未设置",一个"设为空"
//
// ── null 到底是什么?怎么理解 ⭐ ──
// null 是一个【独立的原始值】,自成一类,类里只有它一个成员;含义="有意的空"。
// • typeof null // 'object' ← 这是 1995 年的历史 bug(底层用全 0 表示,被误判)。
// 全世界老代码都依赖它了,改了会崩,只好将错就错。→ null 不是对象,是原始值!
// (呼应 00-concepts/value-vs-reference.md:null 明列在"原始值"栏)
// • 怎么判断是不是 null?直接用 === 就行,可靠(不像 NaN 连自己都不等):
// if (x === null) { ... } // ✅ null === null 成立
// // ❌ 别用 typeof x === 'object' 判 null:{}、[] 也是 'object',会误伤
// • 什么时候用:"这位置将来会有个对象,但现在还没有"→ 用 null 明确占位:
// let currentUser = null // 还没登录,主动标记"当前无用户"
// currentUser = { name: 'Tom' } // 登录后再填
// 比放 undefined 更能表达"我【知道】这里暂时空,不是忘了"(agent 代码常见)。
//
// 7. NaN / Infinity 都属于 number ⭐
// NaN(Not a Number)名字唬人,但它自己的类型偏偏是 number ——
// 它是 number 家族里表示"无效数字"的特殊值,不是独立的第 8 种类型。
// typeof NaN // 'number' ← 名叫"不是数字",类型却是数字!
// typeof Infinity // 'number'
// ┌────────────────┬──────────┬──────────────────────────────────┐
// │ 特殊 number 值 │ 含义 │ 怎么产生 │
// ├────────────────┼──────────┼──────────────────────────────────┤
// │ NaN │ 无效数字 │ 'abc' - 1、Number('hi') 转换失败 │
// │ Infinity │ 正无穷 │ 1 / 0 │
// │ -Infinity │ 负无穷 │ -1 / 0 │
// └────────────────┴──────────┴──────────────────────────────────┘
// 大坑:NaN 不等于它自己 ⭐
// NaN === NaN // false ← 连自己都不等于!
// 所以不能用 === NaN 判断,要用专门函数:
// Number.isNaN(x) // 这才是判断"是不是 NaN"的正确方法
//
// ✅ 本主题要点回顾
// 1. 变量 = 给数据起名字
// 2. 优先 const,要改才用 let,别用 var ⭐
// 3. 命名:小驼峰、有意义、不能数字开头
// 4. 常用类型:number / string / boolean,外加 undefined / null
// (null 是"有意的空",独立原始值;判断用 === null,typeof null==='object' 是坑)
// 5. typeof 查类型(注意 typeof null === 'object' 是坑)
// 6. NaN / Infinity 都属于 number;NaN !== NaN,判断用 Number.isNaN() ⭐
// ═════ 示范 ═════
// ── 1. let 可以改,const 不能改 ⭐ ─────────────────
let age = 18
console.log(age) // 18
age = 19 // let 允许重新赋值
console.log(age) // 19
const PI = 3.14
console.log(PI) // 3.14
// PI = 3.15 // ← 取消注释会报错:Assignment to constant variable
// ── 2. const 首选,会变的才用 let ─────────────────
let count = 0
count = count + 1
count = count + 1
console.log('count =', count) // count = 2
// ── 3. 常用数据类型 ───────────────────────────────
const myNumber = 100 // number
const myString = 'JavaScript' // string
const myBool = true // boolean
console.log(myNumber, myString, myBool) // 100 JavaScript true
// ── 4. typeof 查类型 ⭐ ───────────────────────────
console.log(typeof 18) // number
console.log(typeof 'hi') // string
typeof 'hi' // ← 这行会算,但文件里【看不到】任何输出(见第 8 组)
console.log(typeof true) // boolean
console.log(typeof undefined) // undefined
console.log(typeof null) // object ← 历史遗留坑,记住即可
// ── 5. undefined vs null ─────────────────────────
let x // 声明但没赋值
console.log(x) // undefined ← 系统默认
let y = null // 主动清空
console.log(y) // null ← 你给的
// ── 6. 小驼峰命名 + 反引号拼句子(复习上一章) ──────
const firstName = 'Asabeneh'
const country = 'Finland'
console.log(`${firstName} lives in ${country}`) // Asabeneh lives in Finland
// ── 7. 数组的 typeof 是 'object' ⭐ ───────────────
// 数组不是独立类型,本质是特殊的 object,所以 06-arrays 才专门讲
const arr = [1, 2, 3]
console.log(typeof arr) // object ← 不是 'array'!
console.log(Array.isArray(arr)) // true ← 想确认是不是数组用这个
// ── 8. "能算" ≠ "能看见":文件里必须 console.log ⭐ ─
typeof 'hi' // 这行会算出 'string',但在文件里【看不到】任何东西
console.log(typeof 'hi') // string ← 用 console.log 包起来才显示
// typeof 是运算符不是函数 → 后面加不加括号都行:
console.log(typeof 'hi') // string
console.log(typeof ('hi')) // string ← 这括号只是分组,不是函数调用
// ═════ 轮到你 ═════
// 👇 轮到你(把 ??? 换掉,取消注释,跑一下对答案)
// 练习 A:声明一个 const 存你的名字,再用反引号打印 "我叫 xxx"
// const myName = '???'
// console.log(`我叫 ${myName}`)
// 练习 B:声明一个 let 叫 score = 0,让它加 10,再加 5,打印结果(应为 15)
// let score = ???
// score = ???
// score = ???
// console.log('score =', score)
// 练习 C:用 typeof 查一下 3.14 是什么类型,打印出来(应为 number)
// console.log(typeof ???)
const myName = "Jaydon"
console.log(`我叫 ${myName}`) // 我叫 Jaydon ← 是 ${} 不是 &{} !
let score = 0
score += 10
score += 5
console.log(score)

@ -0,0 +1,198 @@
// 03 · 运算符
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区每行后注释是【预期输出】,跑完自己对答案
// ═════ 概念总结 ═════
//
// 1. 算术运算符
// ┌──────┬────────────┬──────────┬──────────────────────────┐
// │ 符号 │ 含义 │ 例子 │ 结果 │
// ├──────┼────────────┼──────────┼──────────────────────────┤
// │ + │ 加 │ 2 + 3 │ 5 │
// │ - │ 减 │ 5 - 2 │ 3 │
// │ * │ 乘 │ 4 * 3 │ 12 │
// │ / │ 除 │ 10 / 4 │ 2.5(JS 除法不取整!) │
// │ % │ 取余(模) │ 10 % 3 │ 1 │
// │ ** │ 幂(次方) │ 2 ** 3 │ 8 │
// └──────┴────────────┴──────────┴──────────────────────────┘
// 两个初学重点:
// - JS 里 10 / 4 = 2.5,不像有些语言整数相除会取整。
// - % 取余超常用:判断奇偶(n % 2 === 0 就是偶数)、循环里做周期。
//
// ⚠️ 负数取余的坑:判奇偶要用 === 0 不要用 === 1
// JS 里取余结果的符号跟着"被除数"走,负数取余是负的:
// 7 % 2 // 1 -7 % 2 // -1 ← 不是 1!符号跟着 -7
// 所以判奇偶:
// n % 2 === 0 // ✅ 判偶数,对正负数都成立
// n % 2 === 1 // ❌ 判奇数,遇负数会漏:-7%2 是 -1,=== 1 得 false → 误判成偶数
// 结论:判奇偶一律用 n % 2 === 0(判偶),再用 else 处理奇数。别用 === 1。
//
// 2. 赋值运算符
// = 是"赋值"(把右边存进左边),下面是简写:
// ┌────────┬──────────────┬────────┐
// │ 写法 │ 等价于 │ 说明 │
// ├────────┼──────────────┼────────┤
// │ x += 5 │ x = x + 5 │ 加等于 │
// │ x -= 5 │ x = x - 5 │ 减等于 │
// │ x *= 2 │ x = x * 2 │ 乘等于 │
// │ x /= 2 │ x = x / 2 │ 除等于 │
// └────────┴──────────────┴────────┘
//
// 3. 比较运算符 → 结果是布尔值 ⭐
// ┌───────────────┬──────────────────────────────┬──────────┬──────────┐
// │ 符号 │ 含义 │ 例子 │ 结果 │
// ├───────────────┼──────────────────────────────┼──────────┼──────────┤
// │ > < >= <= │ 大小比较 │ 5 > 3 │ true │
// │ === │ 严格等于(值和类型都要相等) │ 1 === 1 │ true │
// │ !== │ 严格不等 │ 1 !== 2 │ true │
// │ == │ 松散等于(会偷偷转类型) │ 1 == '1' │ true ⚠️ │
// └───────────────┴──────────────────────────────┴──────────┴──────────┘
// ⭐⭐ 最重要的一条规则:永远用 ===,别用 ==
// == 会做"隐式类型转换",制造诡异结果:
// 1 == '1' // true ← 数字 1 和字符串 '1' 被判相等?!
// 1 === '1' // false ← === 看类型,数字 ≠ 字符串,正确
// 0 == '' // true ← 更离谱
// 记死这条:比较一律用 === 和 !==。这是 JS 最著名的坑之一。
//
// 4. 逻辑运算符
// ┌──────┬───────────┬──────────────────┐
// │ 符号 │ 含义 │ 口诀 │
// ├──────┼───────────┼──────────────────┤
// │ && │ 与(AND) │ 都为真才真 │
// │ || │ 或(OR) │ 有一个真就真 │
// │ ! │ 非(NOT) │ 真假反转 │
// └──────┴───────────┴──────────────────┘
// true && false // false true || false // true !true // false
// 常用于条件判断:age >= 18 && hasTicket(成年且有票)。
//
// 5. 一个坑:+ 对字符串是"拼接" ⭐
// 1 + 2 // 3 ← 数字相加
// '1' + '2' // '12' ← 字符串拼接
// '1' + 2 // '12' ← 只要有一个是字符串,就拼接!
// 1 + '2' + 3 // '123' ← 从左到右,一旦变字符串后面全拼
// 其他运算符(- * /)不会拼接,反而会把字符串转成数字再算:
// '6' - 2 === 4、'6' * 2 === 12。只有 + 特殊。
//
// 6. 类型转换:显式 vs 隐式 ⭐⭐
// 上面这些坑的根源,都是 JS 在偷偷"转类型"。
// 隐式转换(JS 自作主张,坑的来源)—— 你没让它转,它自己转了:
// '5' + 3 // '53' ← 偷偷把 3 转成字符串,拼接
// '5' - 3 // 2 ← 偷偷把 '5' 转成数字
// 0 == '' // true ← 偷偷都转成数字比较
// 'abc' - 1 // NaN ← 转不成数字 → NaN(会像病毒一样传染后续计算)
// 显式转换(你主动写,推荐 ✅)—— 意图清楚、无意外:
// Number('42') // 42 Number('abc') // NaN ← 转不了就是 NaN
// parseInt('42px')// 42 parseFloat('3.14') // 3.14
// String(42) // '42'
// Boolean('') // false Boolean('hi') // true Boolean(0) // false
// 方向性差异(记住):
// - 数字 → 字符串:几乎总能成功
// - 字符串 → 数字:可能失败,失败得 NaN(处理用户输入时要留心)
// 原则:能用显式(Number()/String())就别靠隐式。
//
// 7. TypeScript 能克服这些坑吗?—— 部分能 ⭐
// TS 在写代码时(编译期)检查类型,能挡一大类,但不是全部:
// ┌──────────────────────────────────────────────┬───────────────┐
// │ 坑 │ TS 能挡吗 │
// ├──────────────────────────────────────────────┼───────────────┤
// │ '5' - 3(类型不匹配的运算) │ ✅ 编译期报错 │
// │ 0 == ''(乱比较) │ ✅ 报错 │
// │ '5' + 3(拼接,语法合法) │ ❌ 不管 │
// │ 外部数据(输入/请求/JSON.parse)类型错 │ ❌ 只信标注 │
// │ 运行时才暴露的问题 │ ❌ 类型已消失 │
// └──────────────────────────────────────────────┴───────────────┘
// 心智模型:TS 是"写代码时的第一道墙",挡掉大部分手滑;但 + 拼接、外部数据
// 这些盲区,仍要靠显式转换 + 校验输入兜底。理解 JS 转换原理,是用好 TS 的前提。
//
// ✅ 本主题要点回顾
// 1. 算术:/ 不取整、% 取余判奇偶、** 次方
// 2. 赋值简写:+= -= *= /=
// 3. 比较永远用 === / !==,别用 == ⭐⭐
// 4. 逻辑:&&(都真)、||(有一真)、!(反转)
// 5. + 遇字符串会拼接,其他运算符会转数字 ⭐
// 6. 能用显式转换(Number()/String())就别靠隐式 ⭐
// 7. TS 能挡"类型不匹配"的坑,但挡不住 + 拼接和外部数据 —— 显式转换的习惯不能丢
// ═════ 示范 ═════
// ── 1. 算术运算符 ─────────────────────────────────
console.log(2 + 3) // 5
console.log(5 - 2) // 3
console.log(4 * 3) // 12
console.log(10 / 4) // 2.5 ← JS 除法不取整!
console.log(10 % 3) // 1 ← 取余
console.log(2 ** 3) // 8 ← 2 的 3 次方
// ── 2. % 取余判断奇偶 ⭐ ──────────────────────────
console.log(10 % 2) // 0 ← 余 0 → 偶数
console.log(7 % 2) // 1 ← 余 1 → 奇数
console.log(10 % 2 === 0) // true ← 判断偶数的标准写法
// ── 3. 赋值简写 ───────────────────────────────────
let x = 10
x += 5 // 等价 x = x + 5
console.log(x) // 15
x *= 2 // 等价 x = x * 2
console.log(x) // 30
// ── 4. 比较运算符 → 布尔值 ────────────────────────
console.log(5 > 3) // true
console.log(5 <= 4) // false
console.log(1 === 1) // true
console.log(1 !== 2) // true
// ── 5. === vs == 的坑 ⭐⭐(最重要)────────────────
console.log(1 === '1') // false ← 严格:类型不同 → 不等(正确做法)
console.log(1 == '1') // true ← 松散:偷偷转类型 → 相等(危险!)
console.log(0 == '') // true ← == 的离谱行为
// 结论:永远用 ===,别用 ==
// ── 6. 逻辑运算符 ─────────────────────────────────
console.log(true && false) // false ← 都真才真
console.log(true || false) // true ← 有一真就真
console.log(!true) // false ← 反转
const age = 20
const hasTicket = true
console.log(age >= 18 && hasTicket) // true ← 成年且有票
// ── 7. + 遇字符串会拼接 ⭐ ────────────────────────
console.log(1 + 2) // 3 ← 数字相加
console.log('1' + '2') // '12' ← 字符串拼接
console.log('1' + 2) // '12' ← 有一个字符串就拼接
console.log(1 + '2' + 3) // '123' ← 从左到右,变字符串后全拼
// 其他运算符反而会把字符串转成数字:
console.log('6' - 2) // 4 ← - 让 '6' 变数字 6
console.log('6' * 2) // 12 ← * 同理
// ═════ 轮到你 ═════
// 👇 轮到你(把 ??? 换掉,取消注释,跑一下对答案)
// 练习 A:判断 15 是不是偶数(用 % 和 ===),应打印 false
// console.log(15 % 2 === 0)
// 练习 B:一个人 age = 16,判断"成年 且 有票",应打印 false
// const myAge = ???
// const ticket = true
// console.log(??? >= 18 && ticket)
// 练习 C:猜一下 '5' + 3 和 '5' - 3 分别是什么?先猜再取消注释验证
// console.log('5' + 3) // 你猜:___
// console.log('5' - 3) // 你猜:___
// 练习 A:判断 15 是不是偶数 → 要用 === 才是"判断"
console.log(15 % 2 === 0) // false ← 15%2 是 1,1===0 为 false
console.log(15 % 2) // 1 ← 对比:这只是算余数,不是判断
// 练习 B:age=16,判断"成年(>=18)且有票"
const myAge = 16
const ticket = true
console.log(myAge >= 18 && ticket) // false ← 16 不够 18,&& 整体为 false
// 练习 C:先猜后验(你猜的 '53' 和 2 都对!)
console.log('5' + 3) // '53' ← + 遇字符串 → 拼接
console.log('5' - 3) // 2 ← - 把 '5' 转成数字 → 5-3

@ -0,0 +1,160 @@
// 04 · 字符串常用方法
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区带预期输出;轮到你区只给要求,自己写
// ═════ 概念总结 ═════
//
// 字符串(string)是一串文字。JS 给它内置了很多"方法"(method),
// 可以查长度、截取、替换、大小写转换等。
//
// 1. 长度:.length(是属性,不是方法)⭐
// .length 后面不加括号,因为它是"属性"不是"方法"。
// 'hello'.length // 5
// 区分:.length 是属性(不加括号);.toUpperCase() 是方法(加括号)。
// 判断法:方法是"动作",要加 ();属性是"数据",不加。
//
// 2. 大小写
// ┌────────────────┬────────┬────────────────────────────┐
// │ 方法 │ 作用 │ 例子 │
// ├────────────────┼────────┼────────────────────────────┤
// │ .toUpperCase() │ 转大写 │ 'hi'.toUpperCase() → 'HI' │
// │ .toLowerCase() │ 转小写 │ 'HI'.toLowerCase() → 'hi' │
// └────────────────┴────────┴────────────────────────────┘
//
// 3. 按位置取字符
// const s = 'hello'
// s[0] // 'h' ← 下标从 0 开始!
// s[1] // 'e'
// s[s.length - 1] // 'o' ← 最后一个字符的通用写法
// 关键:下标从 0 开始,第 1 个字符是 s[0],最后一个是 s[length-1]。
//
// 4. 截取:.slice(start, end) ⭐
// 从 start 截到 end(不含 end)。
// const s = 'JavaScript'
// s.slice(0, 4) // 'Java' ← 取下标 0,1,2,3(不含 4)
// s.slice(4) // 'Script' ← 省略 end 则取到结尾
// s.slice(-6) // 'Script' ← 负数从右边数
// "不含 end"是初学最容易错的点:slice(0, 4) 拿的是 4 个字符,不是 5 个。
//
// 5. 查找
// ┌──────────────────┬──────────────┬───────────────────────────────┐
// │ 方法 │ 作用 │ 返回 │
// ├──────────────────┼──────────────┼───────────────────────────────┤
// │ .includes('x') │ 是否包含 │ true/false │
// │ .indexOf('x') │ 首次出现下标 │ 数字;找不到返回 -1 │
// │ .startsWith('x') │ 是否以…开头 │ true/false │
// │ .endsWith('x') │ 是否以…结尾 │ true/false │
// └──────────────────┴──────────────┴───────────────────────────────┘
// 'hello'.includes('ell') // true
// 'hello'.indexOf('l') // 2
// 'hello'.indexOf('z') // -1 ← 找不到是 -1,不是报错
//
// 6. 替换与去空格
// 'a-b-c'.replace('-', '+') // 'a+b-c' ← 只换第一个
// 'a-b-c'.replaceAll('-', '+') // 'a+b+c' ← 全换
// ' hi '.trim() // 'hi' ← 去掉两端空格
//
// 7. 拆分:.split() ⭐(字符串 → 数组)
// 'a,b,c'.split(',') // ['a', 'b', 'c'] ← 按逗号拆成数组
// 'hello'.split('') // ['h','e','l','l','o'] ← 拆成单个字符
// split 是字符串通向数组的桥梁,后面数组章会经常一起用。
//
// ⚠️ 一个重要特性:字符串不可变(immutable)
// 字符串方法都不会改原字符串,而是返回一个新字符串。
// let s = 'hello'
// s.toUpperCase() // 'HELLO'(返回新的)
// console.log(s) // 'hello' ← 原来的没变!
// s = s.toUpperCase() // 想保留结果,得重新赋值
// 记住:想要变化生效,必须把返回值赋回去(或存到新变量)。
//
// ✅ 本主题要点回顾
// 1. .length 是属性(不加括号);方法要加 ()
// 2. 下标从 0 开始,最后一个是 [length-1]
// 3. .slice(start, end) 截取,不含 end ⭐
// 4. .includes / .indexOf(找不到返回 -1)
// 5. .split() 把字符串拆成数组 ⭐
// 6. 字符串不可变:方法返回新串,想生效要赋值回去 ⭐
// ═════ 示范 ═════
// ── 1. 长度 .length(属性,不加括号)──────────────
console.log('hello'.length) // 5
// ── 2. 大小写 ─────────────────────────────────────
console.log('hi'.toUpperCase()) // HI
console.log('HI'.toLowerCase()) // hi
// ── 3. 按下标取字符(从 0 开始)────────────────────
const s = 'hello'
console.log(s[0]) // h
console.log(s[s.length - 1]) // o ← 最后一个字符
// ── 4. 截取 .slice(start, end),不含 end ⭐ ────────
const lang = 'JavaScript'
console.log(lang.slice(0, 4)) // Java ← 取 0,1,2,3(不含4)
console.log(lang.slice(4)) // Script ← 到结尾
console.log(lang.slice(-6)) // Script ← 负数从右数
// ── 5. 查找 ───────────────────────────────────────
console.log('hello'.includes('ell')) // true
console.log('hello'.indexOf('l')) // 2
console.log('hello'.indexOf('z')) // -1 ← 找不到是 -1
// ── 6. 替换 / 去空格 ──────────────────────────────
console.log('a-b-c'.replace('-', '+')) // a+b-c ← 只换第一个
console.log('a-b-c'.replaceAll('-', '+')) // a+b+c ← 全换
console.log(' hi '.trim()) // hi ← 去两端空格
// ── 7. 拆分 .split() → 数组 ⭐ ────────────────────
console.log('a,b,c'.split(',')) // ['a', 'b', 'c']
console.log('hello'.split('')) // ['h','e','l','l','o']
// ── 8. 字符串不可变 ⭐ ────────────────────────────
let word = 'hello'
word.toUpperCase() // 返回新串,但没接住
console.log(word) // hello ← 原串没变!
word = word.toUpperCase() // 赋值回去才生效
console.log(word) // HELLO
// 想直接改某个字符?改不动 —— 字符串不可变
let text = 'hello'
text[0] = 'x' // 非严格模式:这行被静默忽略;严格模式/ESM:直接抛 TypeError
console.log(text) // hello ← 没变!要改只能整串重新赋值(见上面第 8 组)
// ═════ 轮到你 ═════
// 👇 轮到你(只给要求,自己写代码)
// 练习 A:有字符串 const city = 'Helsinki'
// ① 打印它的长度
// ② 打印它的大写形式
// ③ 打印它的第一个字符
const city = 'Helsinki'
console.log(city.length)
console.log(city.toUpperCase())
console.log(city[0])
// 练习 B:有 const email = 'user@example.com'
// ① 用 includes 判断它是否包含 '@'(应得 true)
// ② 用 split 按 '@' 拆开,打印结果(想想会得到什么形状)
const email = 'user@example.com'
console.log(email.includes('@'))
console.log(email.split('@'))
// 练习 C:有 const raw = ' JavaScript '(两端有空格)
// 先去掉两端空格,再转成小写,最后打印。
// 提示:去空格和转小写各有一个方法,两个可以【链式】连着写(前一个的结果接着点下一个)
const raw = ' JavaScript '
console.log(raw.trim().toLowerCase())
// 练习 D(动脑):const name = 'javascript'
// 只用你学过的方法,把它变成首字母大写的 'Javascript' 再打印。
// 提示:第一个字符大写 + 剩下的部分,用 + 拼起来。
const name = 'javascript'
console.log(name[0].toUpperCase() + name.slice(1, name.length))

@ -0,0 +1,204 @@
// 05 · 条件与布尔
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区带预期输出;轮到你区只给要求,自己写
// ═════ 概念总结 ═════
//
// 程序会"做决定"—— 根据条件走不同分支。这是代码从"一条直线"变成"有逻辑"的开始。
//
// 1. if / else if / else
// const age = 20
// if (age >= 18) {
// console.log('成年')
// } else if (age >= 13) {
// console.log('青少年')
// } else {
// console.log('儿童')
// }
// - 条件写在 () 里,结果要是 true/false(或真假值,见第 4 节)
// - {} 里是满足时执行的代码
// - 从上往下,命中第一个成立的就停,后面不再判断
//
// 2. 三元运算符 ? : ⭐
// if/else 的极简版,一行搞定"二选一",而且有返回值可以赋给变量:
// const status = age >= 18 ? '成年' : '未成年'
// // 条件 ? 真时的值 : 假时的值
// 适合:简单的二选一赋值。
// 别滥用:逻辑复杂就用 if,别把三元套三元,可读性差。
//
// 3. switch(多分支)
// 当一个值要和很多固定选项比对时,比一长串 else if 清爽:
// switch (day) {
// case 1:
// console.log('周一')
// break // ← 别忘!否则会"穿透"继续执行下面的 case
// case 3:
// console.log('周三')
// break
// default:
// console.log('其他')
// }
// 为什么每个 case 都要 break —— "穿透"(fall-through)⭐
// 关键:case 不是"独立的盒子",而是"跳转的入口(标签)"。匹配成功后程序从那个
// 入口跳进去,然后一路往下执行,撞到 break 才跳出。不写 break 就会"掉进"下一个 case。
// day = 1 时若各 case 都不写 break → 输出:周一 周二 周三(全跑了)
// 跨语言对比(纠正常见误解):
// ┌───────────────────────────┬──────────────────────────────────────┐
// │ 语言 │ 要 break 吗 │
// ├───────────────────────────┼──────────────────────────────────────┤
// │ C / C++ / Java / JS │ ✅ 都要(JS 的 switch 就是抄 C 的) │
// │ Python │ 没有传统 switch(match 机制不同,不穿透)│
// └───────────────────────────┴──────────────────────────────────────┘
// 记反了很常见:C++/Java 和 JS 一样都要写 break,不是自动 break。
// 穿透的唯一正当用法:多个 case 共用逻辑(故意省略 break):
// case 6:
// case 7:
// console.log('周末') // 6 和 7 都到这
// break
// 💡 因为"忘写 break"太容易出 bug,现代 JS 常少用 switch,改用 if/else if
// 或对象查表(学对象后会讲)。知道 switch 怎么回事即可,实战非首选。
//
// 4. 真假值 truthy / falsy ⭐⭐(之前埋的伏笔)
// if() 里不一定非要放 true/false —— 任何值都会被 JS 自动判成"真"或"假"。
// 假值(falsy)—— 常记这 6 个就够:
// false 0 ''(空字符串) null undefined NaN
// (严格说还有 0n(BigInt 的零)和 -0,都少见,先把上面 6 个记牢)
// 真值(truthy)—— 除了上面 6 个,其他全是真:
// 包括非零数字、非空字符串、[] 空数组、{} 空对象(注意!空数组空对象是真)
// if ('hello') … // 会执行
// if (0) … // 不执行
// if ([]) … // 会执行(容易错)
// 用途:快速判断"有没有值"(空串是假 → 走 else 分支)。
//
// 5. 短路:&& / || 返回的是"操作数原值",不是布尔 ⭐⭐
// 关键认知:JS 的 &&/|| 不返回 true/false,而是返回其中一个操作数本身。
// 靠的是短路求值:能确定结果时立刻停下,返回"决定胜负的那个值"。
// || :返回第一个真值(找到真就停);全假返回最后一个
// '' || '匿名' // '匿名' ← 前面假,继续,返回后面
// 'Tom' || '匿名' // 'Tom' ← 前面就是真,立刻返回,'匿名'看都不看
// 0 || 100 // 100
// && :返回第一个假值(找到假就停);全真返回最后一个
// 'Tom' && '有值' // '有值' ← 前面真,继续,返回后面
// '' && '有值' // '' ← 前面就是假,立刻返回它
// 1 && 2 && 3 // 3 ← 全真,返回最后一个
// 跨语言对比(纠正误解):
// ┌─────────────────────────────┬───────────────────────────────────┐
// │ 语言 │ &&/|| 返回 │
// ├─────────────────────────────┼───────────────────────────────────┤
// │ JS / Python / Ruby / Lua │ 操作数原值(如 '' || 'x' → 'x') │
// │ C / C++ / Java / C# │ 严格 boolean │
// └─────────────────────────────┴───────────────────────────────────┘
// 记反了很常见:Python 和 JS 一样返回原值;Java/C++ 才返回布尔。
// 两个超常用惯用法 ⭐:
// const name = userInput || '游客' // ① || 设默认值:没填(空串是假)就用 '游客'
// user && user.login() // ② && 做"守卫":user 存在才调用 login,避免报错
// // (React 里满地都是: 条件 && <组件/> → 条件成立才渲染)
// 想要纯布尔?用 !!:
// !!'Tom' // true !!'' // false !!0 // false
// ! 取反得布尔,!! 再反回来 → 纯 true/false。
// 💡 为什么这样设计:JS 到处用 truthy/falsy 而非死板布尔(见第 4 节),
// 所以 &&/|| 返回原值才顺理成章 —— 这两个知识点是一套的。
//
// ✅ 本主题要点回顾
// 1. if / else if / else:命中第一个成立的就停
// 2. 三元 条件 ? 真值 : 假值:简单二选一,有返回值 ⭐
// 3. switch:多分支,每个 case 别忘 break
// 4. falsy 常记 6 个:false 0 '' null undefined NaN(另有 0n/-0,少见);其余全 truthy(空数组空对象是真!)⭐⭐
// 5. &&/|| 返回操作数原值(非布尔):|| 取第一个真值(设默认值),&& 取第一个假值(做守卫);
// 要纯布尔用 !!。Python 同理,Java/C++ 才返回布尔 ⭐⭐
// ═════ 示范 ═════
// ── 1. if / else if / else ───────────────────────
const age = 20
if (age >= 18) {
console.log('成年') // ← 命中这个,输出:成年
} else if (age >= 13) {
console.log('青少年')
} else {
console.log('儿童')
}
// ── 2. 三元运算符 ? : ⭐ ──────────────────────────
const status = age >= 18 ? '成年' : '未成年'
console.log(status) // 成年
// ── 3. switch(每个 case 别忘 break)───────────────
const day = 3
switch (day) {
case 1:
console.log('周一')
break
case 3:
console.log('周三') // ← 输出:周三
break
default:
console.log('其他')
}
// ── 4. truthy / falsy ⭐⭐ ────────────────────────
// 假值只有 6 个:false 0 '' null undefined NaN
if ('hello') console.log('非空字符串 → 真') // 会执行
if (0) console.log('这行不会执行') // 0 是假
if ([]) console.log('空数组 → 真!') // 会执行(容易错)
if ('') {
console.log('空串是真')
} else {
console.log('空串是假 → 走这') // 会执行
}
// ── 5. 短路:|| 设默认值,&& 前真才看后 ⭐ ─────────
console.log('' || '匿名') // 匿名 ← 前面假,取后面
console.log('Tom' || '匿名') // Tom ← 前面真,直接取
console.log(true && '继续') // 继续
// ═════ 轮到你 ═════
// 👇 轮到你(只给要求,自己写代码)
// 练习 A:给一个 const score = 75
// 用 if/else if/else 判断等级并打印:
// >=90 打印 'A',>=60 打印 'B',否则打印 'C'
const score = 75
if (score >= 90) {
console.log('A')
} else if (score >= 60 ) {
console.log('B')
} else {
console.log('C')
}
// 练习 B:把练习 A 里"是否及格(>=60)"改用【三元运算符】写,
// 得到 '及格' 或 '不及格',打印出来
console.log(score >= 60 ? '及格' : '不及格')
// 练习 C(truthy/falsy):有 const username = ''
// 用 if 判断:如果 username 有值就打印它,否则打印 '游客'
// (想想空字符串是真还是假)
const username = ''
if (username) {
console.log(username)
} else {
console.log('游客')
}
// 练习 D(短路,动脑):用 || 把练习 C 改成【一行】,
// 效果一样:有名字用名字,没名字用 '游客'
console.log(username || '游客')
// 练习 E(挑战):写一个三元,判断 num 是奇数还是偶数,
// const num = 7,打印 '奇数' 或 '偶数'(回忆 % 取余)
const num = 7
console.log(num % 2 === 1 ? '奇数' : '偶数')
if (-3) {
console.log('true')
} else {
console.log('false')
}

@ -0,0 +1,213 @@
// 06 · 数组(Array)
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区带预期输出;轮到你区只给要求,自己写
// ═════ 概念总结 ═════
//
// 数组 = 有序的一组值,用 [] 包起来。JS 里最常用的数据结构。
//
// 0. 回顾:数组是"引用类型" ⭐
// 复习 00-concepts/value-vs-reference.md:数组是对象/引用类型,
// - typeof [1,2,3] → 'object'(不是 'array')
// - 判断是不是数组用 Array.isArray(arr)
// - 可以原地修改(和字符串不可变相反)
// - let b = a 是共享同一个数组,不是复制
//
// 1. 创建 & 取值
// const fruits = ['apple', 'banana', 'cherry']
// fruits[0] // 'apple' ← 下标从 0 开始
// fruits[2] // 'cherry'
// fruits[fruits.length - 1] // 'cherry' ← 最后一个
// fruits.length // 3 ← 长度(属性,不加括号)
//
// 2. 增删(会改原数组)⭐
// ┌─────────────┬──────────────────────────────┬────────┐
// │ 方法 │ 作用 │ 位置 │
// ├─────────────┼──────────────────────────────┼────────┤
// │ .push(x) │ 加到末尾 │ 尾 │
// │ .pop() │ 删最后一个(并返回它) │ 尾 │
// │ .unshift(x) │ 加到开头 │ 头 │
// │ .shift() │ 删第一个(并返回它) │ 头 │
// └─────────────┴──────────────────────────────┴────────┘
// 记忆:push/pop 管尾,shift/unshift 管头。这四个都直接改原数组。
//
// 3. 查找
// ┌────────────────┬──────────────┬────────────────────┐
// │ 方法 │ 作用 │ 返回 │
// ├────────────────┼──────────────┼────────────────────┤
// │ .includes(x) │ 是否包含 │ true/false │
// │ .indexOf(x) │ 首次出现下标 │ 数字;找不到 -1 │
// └────────────────┴──────────────┴────────────────────┘
//
// 4. 截取 & 拼接
// .slice(start, end) —— 不改原数组,返回新的(不含 end):
// const a = [1, 2, 3, 4, 5]
// a.slice(1, 3) // [2, 3] ← 和字符串 slice 一样,不含 end
// a.slice(2) // [3, 4, 5]
// .concat() / 展开 —— 合并数组:
// [1, 2].concat([3, 4]) // [1,2,3,4]
// [...[1, 2], ...[3, 4]] // [1,2,3,4] ← 展开语法(后面章节详讲)
//
// 5. ⚠️ .splice() —— 强力但会改原数组
// splice(起点, 删几个, 要插入的...) 能删能插,直接改原数组:
// const a = [1, 2, 3, 4]
// a.splice(1, 2) // 从下标1删2个 → a: [1, 4]
// const b = ['a', 'd']
// b.splice(1, 0, 'b', 'c') // 下标1删0个,插入 b,c → b: ['a','b','c','d']
// 第一个参数是"下标",插入发生在该下标之前 ⭐
// 插入的新元素会占据 index 这个位置,原本在此及以后的元素全部后移。
// 缝隙心智模型(同时解释 slice):把下标看成元素之间的"缝":
// [ first , third ]
// ↑ ↑ ↑
// 缝0 缝1 缝2
// splice(index, 0, x) = 把 x 塞进"缝 index":
// ['first','third'].splice(0, 0, 'x') // ['x','first','third'] 塞缝0=最前
// ['first','third'].splice(1, 0, 'x') // ['first','x','third'] 塞缝1=中间 ✅
// ['first','third'].splice(2, 0, 'x') // ['first','third','x'] 塞缝2=最后(index=length)
// 同一模型解释 slice(1,3):从缝1切到缝3,夹住的是下标1、2 → 所以"不含 end"。
// ⚠️ splice 的返回值是"被删元素",不是修改后的数组:
// const q = ['first', 'third']
// const removed = q.splice(1, 0, 'second')
// console.log(q) // ['first','second','third'] ← 改的是原数组,要打印它
// console.log(removed) // [] ← 返回值是被删的元素,删0个就是空数组
// 所以插入后要 console.log(q)(原数组),别打印 q.splice(...) 的返回值。
// ⚠️ 别把 slice(切片,不改原,返回切出的新数组)和 splice(剪接,改原,
// 返回被删元素)搞混!差一个字母,行为完全不同。
//
// 6. 数组 ↔ 字符串
// ['a', 'b', 'c'].join('-') // 'a-b-c' ← 数组转字符串
// 'a-b-c'.split('-') // ['a','b','c'] ← 字符串转数组(复习 04)
//
// 7. 一个坑:== 比不了数组内容
// 数组是引用类型,=== 比的是"是不是同一个对象",不是内容:
// [1, 2] === [1, 2] // false! ← 两个不同的数组对象
// 比内容要逐个比,或转成字符串比(后面有更好的办法)。
//
// ✅ 本主题要点回顾
// 1. 数组是引用类型(可原地改;let b=a 是共享)⭐
// 2. [0] 取值,下标从 0;.length 长度
// 3. push/pop 管尾,shift/unshift 管头,都改原数组 ⭐
// 4. .slice 不改原(返回切出的新数组);.splice 改原(删/插,返回被删元素,
// 插入后要打印原数组)—— 别搞混 ⭐
// splice 第一个参数是下标,插入在其之前;用"缝隙模型"记(缝index)
// 5. .join() 数组→字符串,.split() 字符串→数组
// 6. [1,2] === [1,2] 是 false(比的是对象不是内容)⭐
// ═════ 示范 ═════
// ── 0. 数组是引用类型(复习)──────────────────────
console.log(typeof [1, 2, 3]) // object ← 不是 'array'
console.log(Array.isArray([1, 2, 3])) // true
// ── 1. 创建 & 取值 ───────────────────────────────
const fruits = ['apple', 'banana', 'cherry']
console.log(fruits[0]) // apple
console.log(fruits[fruits.length - 1]) // cherry
console.log(fruits.length) // 3
// ── 2. 增删:push/pop 管尾,shift/unshift 管头 ⭐ ──
const a = [1, 2, 3]
a.push(4)
console.log(a) // [1, 2, 3, 4]
a.pop()
console.log(a) // [1, 2, 3]
a.unshift(0)
console.log(a) // [0, 1, 2, 3]
a.shift()
console.log(a) // [1, 2, 3]
// ── 3. 查找 ──────────────────────────────────────
console.log([1, 2, 3].includes(2)) // true
console.log([1, 2, 3].indexOf(3)) // 2
console.log([1, 2, 3].indexOf(9)) // -1
// ── 4. slice 不改原(不含 end)────────────────────
const b = [1, 2, 3, 4, 5]
console.log(b.slice(1, 3)) // [2, 3]
console.log(b) // [1, 2, 3, 4, 5] ← 原数组没变
// ── 5. splice 改原(删/插)⚠️ 别和 slice 混 ────────
const c = [1, 2, 3, 4]
c.splice(1, 2) // 从下标1删2个
console.log(c) // [1, 4]
// ── 6. 数组 ↔ 字符串 ─────────────────────────────
console.log(['a', 'b', 'c'].join('-')) // a-b-c
console.log('a-b-c'.split('-')) // ['a', 'b', 'c']
// ── 7. === 比不了内容 ────────────────────────────
console.log([1, 2] === [1, 2]) // false ← 是两个不同对象
// ═════ 轮到你 ═════
// 👇 轮到你(只给要求,自己写代码)
// 练习 A:有 const nums = [10, 20, 30]
// ① 在末尾加上 40
// ② 在开头加上 5
// ③ 打印最终数组和它的长度
// (最终应是 [5, 10, 20, 30, 40],长度 5)
const num = [10, 20, 30]
num.push(40)
num.unshift(5)
console.log(num)
console.log(num.length)
// 练习 B:有 const colors = ['red', 'green', 'blue']
// ① 用 includes 判断有没有 'green'
// ② 打印 'blue' 的下标
// ③ 用 join 把它们拼成 'red, green, blue'(用 ', ' 连接)
const colors = ['red', 'green', 'blue']
console.log(colors.includes('green'))
console.log(colors.indexOf('blue'))
console.log(colors.join(', '))
// 练习 C:有 const letters = ['a', 'b', 'c', 'd', 'e']
// 用 slice 取出中间三个 ['b', 'c', 'd'],并打印。
// 打印完再打印一次 letters 本身,确认它没被改动。
const letters = ['a', 'b', 'c', 'd', 'e']
console.log(letters.slice(1, 4))
console.log(letters)
// 练习 D(动脑):有 const q = ['first', 'third']
// 用 splice 在 'first' 和 'third' 之间插入 'second',
// 让它变成 ['first', 'second', 'third'],打印。
// 提示:splice(位置, 删几个, 插入什么) —— 这里"删几个"是 0
const q = ['first', 'third']
// ❌ splice 返回的是“被删除的元素",不是修改后的数组
// console.log(q.splice(0, 0, 'second'))
// ✅
q.splice(1, 0, 'second')
console.log(q)
// 练习 E(挑战):有 const sentence = 'I love java script'
// ① 用 split 把它拆成单词数组
// ② 用 join 把单词用 '-' 重新连起来
// (最终得到 'I-love-java-script')
const sentence = 'I love java script'
// ❌
// sentence.split(' ')
// console.log(sentence)
// sentence.join('-')
// ❌ 直接写 array = ... 而没声明。这在普通模式下能跑(JS 会偷偷创建一个全局变量),但这是坏习惯,而且埋雷:
// - 严格模式('use strict')下会直接报错:array is not defined
// - 它会"泄漏"成全局变量,以后可能和别处撞名,制造诡异 bug
// array = sentence.split(' ')
// ✅
const array = sentence.split(' ')
console.log(array.join('-'))

@ -0,0 +1,249 @@
// 07 · 对象(Object)
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区带预期输出;轮到你区只给要求,自己写
// ═════ 概念总结 ═════
//
// 对象 = 一组"键值对"(key-value),用 {} 包起来。
// 数组是"有序列表"(按下标),对象是"带标签的数据"(按名字)。
// 用来表示真实世界的东西:一个人、一辆车。
//
// 0. 对象到底是什么?——两层含义 ⭐⭐(总纲)
// 表面:对象 = 键值对(键: 值)的集合,用 {} 装(就是下面 §1 讲的)。
// 本质:JS 的类型分【两大阵营】—— 原始值 vs 对象:
// • 原始值(7 个):number / string / boolean / undefined / null / bigint / symbol
// • 对象(就 1 个大类,却装下几乎其余一切):普通 {}、【数组】、【函数】、Date、正则……
// 它们本质【都是对象】,只是"特殊的对象"。
// 判据:能往它身上挂属性(键:值)的,就是对象 ——
// const arr = [1, 2]; arr.myTag = 'hi' // 数组能挂 → 数组是对象
// const fn = () => {}; fn.count = 5 // 函数能挂 → 函数是对象
// typeof [] // 'object' ← 数组是对象
// typeof (()=>{}) // 'function' ← 函数特殊显示,但它也是对象
// 这一条解释了你早前所有疑问:typeof [] 是 object、函数能当值传/能挂属性、
// {} 叫"对象字面量"、对象是【引用类型】(共享,见 00-concepts/value-vs-reference.md)。
// ⚠️ typeof null 也是 'object' → 那是历史 bug,null 不是对象(见 02 章 §5)。
// 💡 TS 里对象在【值层面完全一样】,只是多一层:可用 interface/type 描述
// "这个对象长什么样"(有哪些键、各是什么类型)—— 第 19 章讲,现在知道有这层即可。
//
// 1. 创建 & 结构
// const person = {
// name: 'Asabeneh', // 键: 值
// age: 100,
// isMarried: true,
// skills: ['HTML', 'CSS', 'JS'], // 值可以是数组
// }
// - 每个 键: 值 之间用逗号隔开
// - 键(key) 是名字(字符串),值(value) 可以是任何类型(数字、字符串、数组、甚至另一个对象)
//
// 2. 取值:两种方式 ⭐
// person.name // 'Asabeneh' ← 方式一:点号 .(最常用)
// person['name'] // 'Asabeneh' ← 方式二:方括号 ['键']
// const key = 'age'
// person[key] // 100 ← 键名存在变量里,只能用方括号
// 记:固定键名用 .name;键名是变量时用 [变量]。
// person.key 和 person[key] 不一样!前者找叫 "key" 的键,后者找变量 key 的值对应的键。
//
// 3. 增 / 改 / 删
// 对象是引用类型(和数组一样),可以原地改:
// const p = { name: 'Tom' }
// p.age = 20 // 增:直接给新键赋值
// p.name = 'Jerry'// 改:给已有键赋新值
// delete p.age // 删:用 delete
// delete 是运算符,专删对象属性 —— 数组别用它 ⭐
// delete 是一元运算符(和 typeof 同类),不是方法 → 写 delete obj.key,不是 obj.delete()。
// delete p.age // ✅ 对象属性
// p.delete('age') // ❌ 对象没有 delete 方法
// ⚠️ 数组虽然是特殊对象、语法上能 delete,但会留下"空洞",不该用:
// const arr = [1, 2, 3]
// delete arr[1] // [1, empty, 3] ← 抹空但不移位、长度还是 3!畸形
// 数组删元素用 splice(见 06 章):arr2.splice(1, 1) // [1, 3] 真删除、前移、长度变 2 ✅
// ┌──────────────┬──────────────────┬────────────────┐
// │ 目标 │ delete 能用吗 │ 该用什么 │
// ├──────────────┼──────────────────┼────────────────┤
// │ 对象属性 │ ✅ 正解 │ delete obj.key │
// │ 数组元素 │ ⚠️ 留空洞,别用 │ splice │
// │ 变量/原始值 │ ❌ 无效 │ —— │
// └──────────────┴──────────────────┴────────────────┘
//
// 4. 键不存在 → undefined(不报错)⭐
// const p = { name: 'Tom' }
// p.height // undefined ← 没这个键,返回 undefined,不报错
// 可以用它判断"有没有某个键":
// if (p.name) … // name 有值 → 真
// if (p.height) … // undefined 是假 → 不执行
//
// 5. 常用工具:Object.keys / values / entries ⭐
// 这些是静态方法(复习 00-concepts/call-forms.md:数据传进去):
// const p = { name: 'Tom', age: 20 }
// Object.keys(p) // ['name', 'age'] ← 所有键
// Object.values(p) // ['Tom', 20] ← 所有值
// Object.entries(p) // [['name','Tom'], ['age',20]] ← 键值对数组
// 注意是 Object.keys(p)(大写 Object,p 传进去),不是 p.keys()。
//
// 6. 检查键是否存在
// 'name' in p // true ← in 运算符
// 'age' in p // false
// p.hasOwnProperty('name') // true
//
// 7. 对象里放函数 = "方法"
// 值如果是函数,就叫这个对象的方法:
// const person = {
// name: 'Tom',
// greet: function () {
// return 'Hi, I am ' + this.name // this 指这个对象自己
// },
// }
// person.greet() // 'Hi, I am Tom'
// this 指"调用这个方法的对象"。这里 person.greet() 里的 this 就是 person。
// (this 是个大话题,后面深入;现在知道"this = 当前对象"即可)
//
// 8. 数组 + 对象 = 最常见的数据形状 ⭐
// 真实数据几乎都是"对象数组":
// const users = [
// { name: 'Tom', age: 20 },
// { name: 'Jerry', age: 18 },
// ]
// users[0].name // 'Tom' ← 先取第0个对象,再取它的 name
// users[1].age // 18
// 这就是后端返回的 JSON、后面高阶函数处理的典型数据。记住"数组套对象"的形状。
//
// ✅ 本主题要点回顾
// 0. 本质:JS 分"原始值 vs 对象"两阵营;数组/函数本质都是对象(能挂属性)⭐⭐
// 1. 对象 = {键: 值} 键值对;表示真实事物
// 2. 取值:.键(固定名)/ [变量](键名是变量)⭐
// 3. 增改直接赋值,删用 delete;是引用类型可原地改
// 4. 键不存在返回 undefined(不报错)⭐
// 5. Object.keys/values/entries(obj) 拿键/值/键值对 ⭐
// 6. 对象里的函数 = 方法,用 this 指自己
// 7. "对象数组"是最常见的数据形状 ⭐
// ═════ 示范 ═════
// ── 1. 创建 & 结构 ───────────────────────────────
const person = {
name: 'Asabeneh',
age: 100,
isMarried: true,
skills: ['HTML', 'CSS', 'JS'],
}
console.log(person) // 整个对象
// ── 2. 取值:点号 vs 方括号 ⭐ ────────────────────
console.log(person.name) // Asabeneh
console.log(person['age']) // 100
const key = 'isMarried'
console.log(person[key]) // true ← 键名在变量里,只能用方括号
console.log(person.skills[0]) // HTML ← 值是数组,继续用下标
// ── 3. 增 / 改 / 删 ──────────────────────────────
const p = { name: 'Tom' }
p.age = 20 // 增
p.name = 'Jerry' // 改
delete p.age // 删
console.log(p) // { name: 'Jerry' }
// ── 4. 键不存在 → undefined ⭐ ────────────────────
console.log(person.height) // undefined ← 不报错
// ── 5. Object.keys / values / entries ⭐ ─────────
const q = { name: 'Tom', age: 20 }
console.log(Object.keys(q)) // ['name', 'age']
console.log(Object.values(q)) // ['Tom', 20]
console.log(Object.entries(q)) // [['name','Tom'], ['age',20]]
// ── 6. 检查键是否存在 ────────────────────────────
console.log('name' in q) // true
console.log('height' in q) // false
// ── 7. 对象里的方法 + this ───────────────────────
const dog = {
name: 'WangCai',
bark: function () {
return this.name + ' says woof'
},
}
console.log(dog.bark()) // WangCai says woof
// ── 8. 对象数组(最常见形状)⭐ ──────────────────
const users = [
{ name: 'Tom', age: 20 },
{ name: 'Jerry', age: 18 },
]
console.log(users[0].name) // Tom
console.log(users[1].age) // 18
// ═════ 轮到你 ═════
// 👇 轮到你(只给要求,自己写代码)
// 练习 A:创建一个对象 book,包含:
// title: 'JavaScript', pages: 300, published: true
// 然后分别用【点号】打印 title,用【方括号】打印 pages
const book = {
title: "JavaScript",
pages: 64,
published: true
}
console.log(book.title)
console.log(book['pages'])
// 练习 B:接着上面的 book
// ① 给它增加一个新键 author,值是你的名字
// ② 把 pages 改成 350
// ③ 删除 published 这个键
// ④ 打印最终的 book
book['author'] = "author"
delete book['published']
console.log(book)
// 练习 C:有 const car = { brand: 'Tesla', color: 'red', year: 2024 }
// ① 用 Object.keys 打印所有键
// ② 用 Object.values 打印所有值
// ③ 判断 car 里有没有 'price' 这个键(用 in)
const cat = { brand: 'Tesla', color: 'red', year: 2024 }
console.log(Object.keys(cat))
console.log(Object.values(cat))
console.log("price" in cat)
// 练习 D:有 const student = { name: 'Amy', grade: 90 }
// 给它加一个方法 describe,调用时返回 'Amy: 90'
// (提示:方法里用 this.name 和 this.grade)
// 写完调用它并打印
// ❌
// const student = { name: 'Amy', grade: 90, describe: () => {
// return this.name + ": " + this.grade
// }}
const student = {
name: 'Amy',
grade: 90,
describe: function() {
return this.name + ": " + this.grade
}
}
console.log(student.describe())
// 练习 E(挑战):有下面这个对象数组
// const people = [
// { name: 'Tom', age: 20 },
// { name: 'Jerry', age: 18 },
// { name: 'Spike', age: 25 },
// ]
// 打印出第 2 个人(Jerry)的年龄,以及第 3 个人的名字。
const people = [
{ name: 'Tom', age: 20 },
{ name: 'Jerry', age: 18 },
{ name: 'Spike', age: 25 },
]
console.log(people[1].age + "\n" + people[2].name)

@ -0,0 +1,143 @@
// 08 · Set 与 Map
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node 本文件
// 示范区带预期输出;轮到你区只给要求,自己写
// ═════ 概念总结 ═════
//
// Set 和 Map 是对"数组/对象"的补充。数组允许重复、对象的键只能是字符串 ——
// 这两个新结构补上了这两个短板。
//
// 1. Set —— 不重复的集合 ⭐
// Set 里的值自动去重,不会有重复项。
// const s = new Set([1, 2, 2, 3, 3, 3])
// console.log(s) // Set(3) {1, 2, 3} ← 重复的自动没了
// 常用操作:
// const s = new Set()
// s.add(1) // 加
// s.add(2)
// s.add(1) // 重复的,无效
// s.has(1) // true ← 查是否存在
// s.delete(2) // 删
// s.size // 1 ← 数量(注意是 size,不是 length!)
// ⚠️ Set/Map 用 .size 数数量,不是数组的 .length。
// 最常见用途:数组去重 ⭐⭐
// const arr = [1, 1, 2, 3, 3]
// const unique = [...new Set(arr)] // [1, 2, 3]
// // ↑ 转成 Set 去重,再用 ... 展开回数组
// 这是一个必会的惯用法:[...new Set(数组)] = 数组去重。
//
// 2. Map —— 更强的键值对 ⭐
// Map 像对象,但键可以是任何类型(对象的键只能是字符串/symbol)。
// const m = new Map()
// m.set('name', 'Tom') // set(键, 值)
// m.set(1, 'number key') // 键可以是数字!
// m.set(true, 'bool key') // 甚至布尔、对象
// m.get('name') // 'Tom' ← 用 get 取值
// m.has(1) // true
// m.delete(true)
// m.size // 2
// Map vs 对象:什么时候用哪个
// ┌──────────┬──────────────────────────┬──────────────────────┐
// │ │ 对象 {} │ Map │
// ├──────────┼──────────────────────────┼──────────────────────┤
// │ 键的类型 │ 只能字符串/symbol │ 任意类型 │
// │ 数量 │ Object.keys(o).length │ m.size(直接) │
// │ 顺序 │ 基本按插入序,整数键会重排│ 严格保证插入顺序 │
// │ 遍历 │ 需 Object.keys 等 │ 直接可迭代 │
// └──────────┴──────────────────────────┴──────────────────────┘
// ⚠️ 对象的"顺序"有个坑:字符串键按【插入顺序】,但【整数样式的键】
// (如 '2'、'10')会被自动提前、按升序排,无视你插入的次序:
// Object.keys({ b: 1, 2: 1, a: 1, 10: 1 }) // ['2','10','b','a']
// // 整数键 2,10 被提前且升序
// Map 则严格按插入序,不管键是什么类型 —— 这才是"键是数字又在意顺序时用 Map"的真正理由。
// 日常大多数场景用对象就够;需要"非字符串键""频繁增删""严格保序"时用 Map。
//
// 3. 创建时初始化
// const s = new Set(['a', 'b', 'c']) // Set:传数组
// const m = new Map([ // Map:传"键值对数组"(和 Object.entries 形状一样!)
// ['name', 'Tom'],
// ['age', 20],
// ])
// m.get('age') // 20
// 呼应 07 章:Object.entries(obj) 返回的正是这种 [[k,v],...] 形状,
// 所以 new Map(Object.entries(obj)) 能把对象转成 Map。
//
// 4. 遍历(先知道,循环章会详讲)
// const s = new Set([1, 2, 3])
// for (const x of s) console.log(x) // 1 2 3
// const m = new Map([['a', 1], ['b', 2]])
// for (const [k, v] of m) console.log(k, v) // a 1 / b 2
//
// ✅ 本主题要点回顾
// 1. Set = 不重复集合;add/has/delete/size ⭐
// 2. [...new Set(arr)] = 数组去重(必会惯用法)⭐⭐
// 3. Map = 键可任意类型的键值对;set/get/has/delete/size ⭐
// 4. Set/Map 数数量用 .size,不是 .length
// 5. Map 保证插入顺序;需非字符串键/保序/频繁增删时用 Map,否则对象够用
// ═════ 示范 ═════
// ── 1. Set 自动去重 ⭐ ────────────────────────────
const s = new Set([1, 2, 2, 3, 3, 3])
console.log(s) // Set(3) {1, 2, 3}
// ── 2. Set 常用操作 ──────────────────────────────
const s2 = new Set()
s2.add(1)
s2.add(2)
s2.add(1) // 重复,无效
console.log(s2.has(1)) // true
s2.delete(2)
console.log(s2.size) // 1 ← 注意是 size 不是 length
// ── 3. 数组去重惯用法 ⭐⭐ ─────────────────────────
const arr = [1, 1, 2, 3, 3]
console.log([...new Set(arr)]) // [1, 2, 3]
// ── 4. Map:键可任意类型 ⭐ ────────────────────────
const m = new Map()
m.set('name', 'Tom')
m.set(1, 'number key')
console.log(m.get('name')) // Tom
console.log(m.get(1)) // number key
console.log(m.has(1)) // true
console.log(m.size) // 2
// ── 5. Map 初始化(键值对数组)───────────────────
const m2 = new Map([
['name', 'Tom'],
['age', 20],
])
console.log(m2.get('age')) // 20
// ── 6. 遍历 ──────────────────────────────────────
for (const x of new Set([1, 2, 3])) console.log('set item:', x) // 1,2,3
for (const [k, v] of m2) console.log('map:', k, v) // name Tom / age 20
// ═════ 轮到你 ═════
// 👇 轮到你(只给要求,自己写代码)
// 练习 A:有 const nums = [5, 3, 5, 2, 3, 1, 1]
// ① 用 Set 给它去重,得到一个新数组并打印
// ② 打印去重后有几个元素
// 练习 B:创建一个空 Set,叫 tags
// ① 依次 add 'js', 'css', 'js'(注意第二个 js 重复)
// ② 打印 tags 的 size(想想是几)
// ③ 判断里面有没有 'css'
// 练习 C:创建一个 Map,叫 scores
// ① 存入:'Amy' → 90,'Bob' → 85
// ② 取出并打印 Amy 的分数
// ③ 打印 scores 里有几对(用 size)
// 练习 D(动脑):有一句话 const text = 'a b a c b a'
// 数一数里面有几个【不重复】的字母(空格不算)。
// 提示:先 split 成数组,想办法过滤掉空格,再用 Set 去重数 size。
// (答案应该是 3:a b c)
// 练习 E(挑战):有 const obj = { name: 'Tom', age: 20 }
// 把这个【对象】转成【Map】,然后打印 Map 的 size。
// 提示:回忆 07 章的 Object.entries,它的返回形状正好能喂给 new Map()

@ -0,0 +1,170 @@
// 09 · 循环(Loops)
// 跑法:Quokka 或 右上角 ▷ / node js_notes/09-loops.js
//
// ═════════════════════ 概念总结 ═════════════════════
//
// 循环 = 重复做一件事。JS 有好几种,按场景选:
//
// 循环种类 用途 建议
// ─────────────────────────────────────────────────
// for 经典计数循环,能控制下标 需要下标时
// while 条件为真就一直循环 不知道次数、靠条件停
// do...while 先做一次再判断 至少执行一次
// for...of 遍历【值】(数组/字符串/Set) ⭐ 最常用,遍历元素
// for...in 遍历【键】(对象的 key) 遍历对象属性
//
// ── 1. for:三段式 ⭐ ────────────────────────────
// for (初始; 条件; 每轮结束后) { ... }
// for (let i = 0; i < 5; i++) → i 从 0 到 4
// i++ 是 i = i + 1 的简写
//
// ── 2. for...of:遍历值(最常用)⭐⭐ ─────────────
// for (const x of [10,20,30]) → x 依次是 10,20,30
// 直接拿到【元素本身】,不用管下标。数组/字符串/Set/Map 都能用。
//
// ── 3. for...in:遍历对象的键 ────────────────────
// for (const key in obj) → key 依次是对象的每个键名
// 注意:for...in 拿到的是【键】,要取值就用 obj[key]
//
// ⚠️ 口诀:of 拿值(value),in 拿键/下标(index/key)
// ⚠️ 别用 for...in 遍历数组(会有意外的坑),数组用 for...of 或 for
//
// ── 4. while / do...while ───────────────────────
// while (条件) { ... } 条件先判断,可能一次都不跑
// do { ... } while (条件) 先跑一次再判断,至少跑一次
// ⚠️ 循环体里必须有让条件最终变假的语句,否则【死循环】
//
// ── 5. break / continue ⭐ ──────────────────────
// break 立刻【跳出】整个循环
// continue 跳过【本轮】剩下的,直接进下一轮
// 默认只作用于【最近的一层】循环。
//
// ── 6. 标签 label:给循环起名字,跳出【外层】(冷门,了解即可)──
// 嵌套循环里,想让 break/continue 作用到外层,先给外层贴个"标签":
// outer: for (...) { // outer: 就是标签,形如 `名字:`
// for (...) {
// if (...) break outer // 直接跳出【外层】,而非最近这层
// }
// }
// 标签语法 `名字:` 平时几乎用不到,但要【知道它存在】——因为它和对象的
// `键:` 长得一样,会在别处坑你(见 10 章:箭头函数 => { name: name } 里
// name: 被当成标签,导致静默返回 undefined)。
//
// ═════ 要点回顾 ═════
// 1. for 需要下标;for...of 遍历值(最常用);for...in 遍历对象键
// 2. of 拿值,in 拿键 —— 别记反 ⭐
// 3. 数组别用 for...in
// 4. while 靠条件停,当心死循环
// 5. break 跳出整个循环,continue 跳过本轮(默认作用于最近一层)⭐
// 6. 标签 `名字:` 可让 break/continue 跳出外层;它和对象 `键:` 同形,别混(见10章坑)
//
// ═════════════════════ 示范 ═════════════════════
// ── 1. for 经典计数 ──────────────────────────────
for (let i = 0; i < 3; i++) {
console.log('for i =', i) // 0, 1, 2
}
// 用 for + 下标遍历数组
const arr = ['a', 'b', 'c']
for (let i = 0; i < arr.length; i++) {
console.log(i, arr[i]) // 0 a / 1 b / 2 c
}
// ── 2. for...of 遍历值(最常用)⭐ ─────────────────
for (const fruit of ['apple', 'banana']) {
console.log('of:', fruit) // apple / banana ← 直接拿到值
}
for (const ch of 'Hi') {
console.log('char:', ch) // H / i ← 字符串也能遍历
}
// ── 3. for...in 遍历对象的键 ─────────────────────
const person = { name: 'Tom', age: 20 }
for (const key of Object.keys(person)) {
console.log('key(of):', key) // name / age
}
for (const key in person) {
console.log('in:', key, '=', person[key]) // name = Tom / age = 20
}
// ── 4. while ─────────────────────────────────────
let n = 3
while (n > 0) {
console.log('while:', n) // 3, 2, 1
n-- // ← 必须让 n 变化,否则死循环
}
// ── 5. break / continue ──────────────────────────
for (let i = 0; i < 5; i++) {
if (i === 3) break // i 到 3 就整个跳出
console.log('break demo:', i) // 0, 1, 2
}
for (let i = 0; i < 5; i++) {
if (i % 2 === 0) continue // 偶数跳过本轮
console.log('continue demo:', i) // 1, 3 ← 只打印奇数
}
// ═════════════════════ 轮到你 ═════════════════════
// (只给要求,自己写代码)
// 练习 A:用 for 循环打印 1 到 5(注意:是 1 到 5,不是 0 到 4)
for (let i = 1; i <= 5; i++) {
console.log(i)
}
// 练习 B:有 const nums = [4, 7, 2, 9]
// 用 for...of 遍历,把每个数字打印出来
const nums = [4, 7, 2, 9]
for (const num of nums) {
console.log(num)
}
// 练习 C:有 const car = { brand: 'Tesla', year: 2024, color: 'red' }
// 用 for...in 遍历,按 "键: 值" 的格式打印每一项
// (例如 brand: Tesla)
const car = { brand: 'Tesla', year: 2024, color: 'red' }
for (const i in car) {
console.log(`${i}` + ": " + car[i]) // ✅
console.log(i + ": ", car[i]) // ❌
console.log(i + ":", car[i]) // ✅
}
// 练习 D:用 for 循环算出 1 + 2 + 3 + ... + 100 的总和,打印结果
// 提示:在循环外先定义一个 let sum = 0,每轮把 i 加进去
// (答案应该是 5050)
let sum = 0
for (let i = 1; i <= 100; i++) {
sum += i
}
console.log(sum)
// 练习 E(动脑):有 const nums = [3, 8, 1, 10, 5]
// 用循环找出里面【最大】的数并打印。
// 提示:先假设第一个是最大(let max = nums[0]),
// 遍历时遇到更大的就更新 max。
const nums_ = [3, 8, 1, 10, 5]
let max = nums_[0]
for (const num of nums_) {
max = num > max? num : max
}
console.log(max)
// 练习 F(挑战):打印 1 到 20,但遇到偶数就跳过(用 continue),
// 只打印奇数。
for (let i = 1; i <= 20; i+=1) {
if (i % 2 == 0) // ❌
if (i % 2 === 0) // ✅
continue // ❌
{continue} // ✅
console.log(i)
}

@ -0,0 +1,313 @@
// 10 · 函数(Functions)
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node js_notes/10-functions.js
// 示范区带预期输出;轮到你区只给要求,自己写
//
// ═════════════════════ 概念总结 ═════════════════════
//
// 函数 = 把一段"要重复用的逻辑"打包起来,起个名字,以后一句话就能调用。
// 它是编程从"流水账"走向"可复用、可组合"的关键。后面的异步、高阶函数、
// 类、agent 循环,全都建立在函数之上 —— 这章是地基中的地基。
//
// ── 1. 两种写法:声明 vs 表达式 ⭐ ────────────────
// // (a) 函数声明(declaration)
// function add(a, b) {
// return a + b
// }
// // (b) 函数表达式(expression)—— 把函数当"值"存进变量
// const add = function (a, b) {
// return a + b
// }
// 区别(初学先记一条):声明会被"提升"(hoisting),定义前就能调用;
// 表达式不会,必须先定义后调用。现代代码更常用【表达式 + 箭头函数】。
//
// ── 函数的"名字" vs 装它的"变量名" ⭐(易混,重点)──
// 三种写法,分清"函数自己叫啥"和"你用哪个名字调用它":
// (1) const xxx = () => {} // 箭头函数本体【匿名】,但赋值给 xxx 后,
// // 引擎自动把它 .name 推断成 'xxx'
// // → 日常说"函数叫 xxx"没问题;调用用 xxx()
// (2) const xxx = function yyy() {} // 【具名函数表达式】:函数自己叫 yyy,xxx 是变量
// // xxx() ✅ 用变量名调用
// // yyy() ❌ ReferenceError!yyy 只在函数【内部】可见
// (3) function yyy() {} // 函数声明:函数就叫 yyy,直接 yyy() 调用(且会提升)
//
// ⭐ 重点(2 里的坑与用途):yyy 这个名字【只在函数体内部能用】,外面看不到。
// 那 yyy 有啥用?主要一个:让函数能在【内部调用自己(递归)】,顺便 debug 时显示名字。
// 平时几乎不这么写,了解即可。
// 💡 呼应练习 F:想用递归让 repeat 调用自己,靠的就是"函数有个内部能引用的名字"——
// 函数声明 function repeat(){} 的 repeat、或具名表达式的名字,都能做到;
// 而纯箭头 const repeat = () => {} 若要递归,只能靠外层变量名 repeat 引用自己。
//
// ── 什么叫"提升(hoisting)"?⭐ ──
// 一句话说透:JS 真正执行前,会先扫一遍代码,把某些声明"吊"到所在作用域的顶端。
// 所以你能在"字面写的位置之前"就用到它。hoisting = 吊起、提起。
// • 函数声明 function f(){} → 【整个函数】(名字+函数体)被提到顶
// → 定义前调用 ✅ 能用
// • 函数表达式 const f = function(){} / 箭头 → 只有【变量名】被登记,
// "= 赋值"留在原地不动 → 定义前调用 ❌ 报错(见下)
//
// ── "名字提了却不让碰",提它有啥用?⭐⭐(易困惑)──
// 提升的用途【不是】让你提前用,而是另外两件事:
// 用途一:一进作用域就把所有声明"登记"好 → "这个名字归本作用域"从第一行
// 起就板上钉钉,不会前半段指外层、后半段指内层(名字归属一致)。
// 用途二:const/let 提升后标记为"未初始化,禁止访问",这段区间叫
// 【TDZ 暂时性死区】。你若在赋值前就用它 → 【当场报错】
// ReferenceError: Cannot access 'x' before initialization。
// 对照老式 var:var 也提升,但预设值是 undefined → 提前用【静默给 undefined】,
// 把 bug 藏起来。const/let 是在这个教训上改的:保留提升(用途一),
// 但把"提前读到 undefined"升级成"提前读就报错"(用途二)。
// → 一句话:提升负责"提前登记名字",TDZ 负责"用早了就当场拦下"。
//
// 实用建议:别去利用提升在定义前调用函数(代码会难读)。养成"先定义后用"
// 的习惯,提升对你就几乎无感 —— 而 const+箭头本来就必须先定义后用。
//
// ── 2. 箭头函数 => ⭐⭐(pi/TS 代码里到处是)────────
// const add = (a, b) => a + b // 无 {}:自动 return
// const add = (a, b) => { return a + b } // 有 {}:必须自己 return
// const square = (n) => n * n
// const hi = () => 'hi' // 没参数也要写空括号 ()
//
// 起点(记住这一句,其余都是推论):
// ⭐⭐ => 右边【期望一个表达式】—— 即"能算出一个值的东西"(a+b、n*n、'hi' 都是)。
// • 你给它一个表达式 → 它就是那个值 → JS 直接把这个值 return 出去(这叫"隐式返回")
// • 所以此处 return 不但多余,而且【非法】:return 是【语句】不是表达式,
// 放在"要表达式的位置"→ 直接语法报错:
// const add = (a, b) => return a + b // ❌ SyntaxError(这里要表达式,return 是语句)
//
// 那要写多行逻辑呢?→ { } 是【逃生舱】:
// 一旦 => 右边以 { 开头,JS 特判它为【函数体代码块】(装语句的地方),不再当表达式看。
// 代码块只是"一堆动作",本身不产出值 → JS 不猜你要交啥 → 必须自己 return,否则返回 undefined。
// 一句话:=> 后给"值(表达式)"就自动还;开了"{代码块}"就得自己 return。
//
// ⚠️ 由上面那句核心推出:对象字面量 { } 也以 { 开头 → 会撞上"代码块"这条特判!
// 想直接返回对象,用 () 裹住,强行回到"表达式"语境:
// const make = (name) => { name: name } // ❌ { 被当代码块 → 返回 undefined
// const make = (name) => ({ name: name }) // ✅ () 逼它当表达式 → 返回对象
// 为什么 ❌ 那行不【报错】只是静默 undefined?因为 { 当代码块后,里面 `name:`
// 被当成【标签语句】(JS 给循环起名字的老语法,形如 `outer:`,见 09 章),
// 语法完全合法 → 不报错却啥也没返回。这种"静默 undefined"最难查。
// ⚠️ 单个参数可省括号(n => n*n),但 0 个或 ≥2 个必须带括号。
// ⚠️ 箭头函数没有自己的 this(复习 07 章坑 L:对象方法别用箭头)。
//
// ── 3. 参数 vs 实参、默认参数 ────────────────────
// function greet(name) { ... } // name 是"形参"(占位)
// greet('Tom') // 'Tom' 是"实参"(真值)
// function greet(name = '游客') { ... } // 默认参数:没传就用默认值
// greet() // name = '游客'
// greet('Tom') // name = 'Tom'
//
// ── 4. return:把结果"交出来" ⭐ ──────────────────
// - return 后面的值就是"函数调用的结果",可以接住:const r = add(1,2)
// - 函数没写 return → 返回 undefined(常见坑:算了但没交出来)
// - return 一执行,函数【立刻结束】,后面的代码不再跑
// ⚠️ return 后面别换行!(复习分号笔记坑2:return 换行会被强行补 ; 返回 undefined)
//
// ── 5. 作用域 scope:变量的"可见范围"⭐ ───────────
// - 函数里 let/const 声明的变量,只在函数内可见(出了函数就不存在)
// - 里面能看到外面(外层变量),外面看不到里面(封装)
// - {} 块级作用域:let/const 只在那对花括号内有效
// function f() { const secret = 1 }
// console.log(secret) // ❌ ReferenceError:出了函数就看不见
//
// ── 6. 函数是"一等公民":能当值传来传去 ⭐⭐ ───────
// 函数本身也是一种值,可以:存进变量、当参数传给别的函数、被返回。
// 把"函数当参数传进去"的那个函数,就叫【高阶函数】(下一章专讲):
// [1,2,3].map(n => n * 2) // 把箭头函数传给 map
// 💡 这正是 agent/pi 的核心套路:注册一堆"工具函数",框架在合适时机回调它们。
//
// ── 为什么函数还"有属性"?因为函数就是【可调用的对象】⭐ ──
// 函数本质是【对象】(复习 07 章 §0),只是比普通对象多一个本事:能被 () 调用。
// 既然是对象,"能挂键值对属性"就是它天生的能力:
// • 自带属性(不是你加的,存"关于函数自己的信息"):
// const greet = (a, b) => a + b
// greet.name // 'greet' ← 函数的名字(上次问"函数叫啥"就是读它)
// greet.length // 2 ← 声明了几个参数
// • 你也能自己挂(因为它是对象)——让函数"随身带状态":
// function counter() { counter.callCount++ }
// counter.callCount = 0
// counter(); counter() // counter.callCount 变成 2
// 一条因果链把这几天的知识串起来 ⭐:
// 函数是一等公民 → 所以它得是个【值(对象)】→ 所以能挂属性、能当参数传
// → 所以才有 map/filter/回调 这套玩法。根子都在"一等公民"这一句。
// 💡 对比 Python:Python 函数【同样】是对象、是一等公民(__name__ 自带属性、
// 也能挂自定义属性、能当参数传 map)。区别只在习惯:Python 给函数挂属性较少见,
// 且更爱用【列表推导式 [n*2 for n in xs]】而非 map。底层道理一致,直觉可迁移。
//
// ═════ 要点回顾 ═════
// 1. 两种写法:function 声明(会提升)/ const = function 表达式(不提升)
// 2. 箭头函数 (a,b) => a+b:单表达式自动 return;没自己的 this ⭐⭐
// 3. 默认参数 name = '游客':没传就用默认
// 4. return 交出结果;不写 return 得 undefined;return 后别换行 ⭐
// 5. 作用域:里能看外,外看不到里;let/const 是块级 ⭐
// 6. 函数能当值传递 → 回调 / 高阶函数(下一章)⭐⭐
// 根源:函数是"可调用的对象"→ 所以有属性(.name/.length)、能当参数;Python 同理
//
// ═════════════════════ 示范 ═════════════════════
// ── 1. 函数声明 ──────────────────────────────────
function add(a, b) {
return a + b
}
console.log(add(2, 3)) // 5
// ── 2. 函数表达式(把函数存进变量)────────────────
const multiply = function (a, b) {
return a * b
}
console.log(multiply(4, 5)) // 20
// ── 2.5 提升 hoisting:声明能"提前用",表达式不能 ⭐ ──
console.log(hoisted()) // works! ← 在定义【上面】就调用,函数声明被整体提升,能用
function hoisted() {
return 'works!'
}
// 对照:const/箭头是表达式,提前用会报错(TDZ)。取消注释验证:
// console.log(notYet()) // ❌ ReferenceError: Cannot access 'notYet' before initialization
// const notYet = () => 'nope'
// ── 3. 箭头函数:三种写法,越来越短 ⭐ ─────────────
const addArrow1 = (a, b) => {
return a + b // 带 {} 要自己写 return
}
const addArrow2 = (a, b) => a + b // 单表达式:自动 return
const square = (n) => n * n // 单参数,可省外层括号(这里保留更清晰)
const sayHi = () => 'hi' // 无参数:空括号不能省
console.log(addArrow1(1, 2)) // 3
console.log(addArrow2(1, 2)) // 3
console.log(square(5)) // 25
console.log(sayHi()) // hi
// ── 4. 默认参数 ──────────────────────────────────
const greet = (name = '游客') => `你好, ${name}`
console.log(greet()) // 你好, 游客 ← 没传,用默认
console.log(greet('Tom')) // 你好, Tom ← 传了,用传的
// ── 4.5 箭头返回对象:{ 的两种身份(呼应概念 2)⭐ ──
const makeBad = (name) => { name: name } // { 被当"代码块",不是对象
console.log(makeBad('Tom')) // undefined ← 坑!啥都没返回
const makeGood = (name) => ({ name: name }) // () 强制当"值" → 真的返回对象
console.log(makeGood('Tom')) // { name: 'Tom' }
// ── 5. return 交出结果;不写 return → undefined ⭐ ─
const noReturn = (x) => {
x * 2 // 算了,但没 return 出来
}
console.log(noReturn(10)) // undefined ← 没交出来!
// return 一执行,函数立刻结束
const firstPositive = (a, b) => {
if (a > 0) return a // a 是正数就直接返回,下面不再跑
return b
}
console.log(firstPositive(5, 9)) // 5
console.log(firstPositive(-1, 9)) // 9
// ── 6. 作用域:里能看外,外看不到里 ⭐ ─────────────
const outer = 'I am outside'
const showScope = () => {
const inner = 'I am inside'
console.log(outer) // I am outside ← 里面能看到外面
console.log(inner) // I am inside
}
showScope()
// console.log(inner) // ← 取消注释会报错:inner is not defined(外面看不到里面)
// ── 7. 函数当值传递(回调的雏形)⭐⭐ ──────────────
// 把一个函数作为参数传给另一个函数
const applyTwice = (fn, x) => fn(fn(x)) // fn 是"传进来的函数"
const inc = (n) => n + 1
console.log(applyTwice(inc, 5)) // 7 ← inc(inc(5)) = 7
// 💡 pi 里注册工具、事件回调,本质就是这样"把函数传进去,框架来调用"
// ═════════════════════ 轮到你 ═════════════════════
// (只给要求,自己写代码)
// 练习 A:写一个【函数声明】叫 double,接收一个数字,返回它的两倍。
// 调用 double(8) 并打印(应为 16)
function double(num) {
return num * 2
}
console.log(double(8))
// 练习 B:把练习 A 改写成【箭头函数】版本(单表达式、自动 return),
// 存进 const doubleArrow,调用并打印
const doubleArrow = num => num * 2
console.log(doubleArrow(8))
// 练习 C:写一个箭头函数 greetUser(name),
// 给 name 一个默认值 'guest',返回 `Hello, xxx`。
// 分别在"传名字"和"不传"两种情况下调用并打印
const greetUser = (name = 'guest') => `Hello, ${name}`
console.log(greetUser())
console.log(greetUser("sample name"))
// 练习 D:写一个函数 max2(a, b),返回两个数里较大的那个。
// 要求用到 return 提前结束(想想练习示范 5 的 firstPositive)。
// 用 max2(3, 9) 和 max2(10, 2) 各测一次
function max2(a, b) {
if ( a >= b) {
return a
}
return b
}
// 练习 E(动脑):下面这段为什么打印 undefined?先想再动手改好它,
// 让它正确返回并打印 triple(7)。(提示:看 return)
// const triple = (n) => { n * 3 }
// console.log(triple(...)) ← 你来补,并修好 triple
const triple = (n) => {return n * 3 }
console.log(triple(7))
// 练习 F(挑战,连接下一章):写一个高阶函数 repeat(fn, times),
// 它把传进来的函数 fn 连续调用 times 次(每次打印一句话即可)。
// 例:repeat(() => console.log('hi'), 3) 应打印三行 hi。
// 提示:times 次 → 用循环;fn 是"传进来的函数",直接 fn() 就能调用
const fn = () => {console.log("执行一次fn函数")}
// ❌ 最初版:把 repeat 和 fn 搞混,fn(fn, times-1) —— fn 不会递归,只执行一次
// const repeat = (fn, times) => {
// if (times === 0) return
// return fn(fn, times -1)
// }
// ❌ 第二版:fn() 在前、判断在后 —— 能跑,但 repeat(fn, 0) 会先打印再停 = 打印 1 次(边界错)
// const repeat = (fn, times) => {
// fn()
// times -= 1
// if (times === 0) return
// repeat(fn, times)
// }
// ✅ 终版:判断在前 → repeat(fn, 0) 直接返回,打印 0 次(边界正确)
const repeat = (fn, times) => {
if (times === 0) return // 先判断:times 为 0 就不打印(edge case 处理对)
fn()
times-=1
// return repeat(fn, times)
repeat(fn, times) // 不需要 return 也行(不需要返回值)
}
repeat(fn, 3)
// 函数也是对象:验证"赋值共享同一个"
let x = fn
let y = x // x、y 指向同一个函数对象(x === y 为 true)
// ❌ 用 .name 验证共享会"看走眼":.name 是只读属性,这次赋值【静默失败】
y.name = "修改y的name"
console.log(x.name) // 'fn' ← 没变!不是"没共享",是 .name 改不动(writable:false)
// ✅ 用普通属性才验证到共享:改 y.foo,x.foo 立刻跟着变
y.foo = "shared!"
console.log(x.foo) // 'shared!' ← 这才证明 x、y 是同一个对象
// .name 为什么改不动?看它的属性描述:writable:false = 只读
console.log(Object.getOwnPropertyDescriptor(x, "name")) // { value:'fn', writable:false, ... }

@ -0,0 +1,161 @@
// 11 · 高阶函数 + 回调(Higher-order functions & callbacks)
// 学练复习一体:概念总结 → 示范 → 轮到你
// 跑法:Quokka / 右上角 ▷ / node js_notes/11-higher-order-functions.js
// 示范区带预期输出;轮到你区只给要求,自己写
//
// ═════════════════════ 概念总结 ═════════════════════
//
// 你在第 10 章练习 F 已经摸到门了:repeat(fn, times) 把【函数当参数传进去】再调用。
// 这一章把这个能力用到最常见的地方:处理数组。这三个方法(map/filter/reduce)是
// pi/TS 真实代码里【出现频率最高】的东西,是读懂真实代码的第一道门槛。
//
// ── 0. 两个词:高阶函数 & 回调 ⭐ ────────────────
// • 回调函数(callback):被你【传给别人、由别人来调用】的那个函数。
// • 高阶函数(higher-order function):【接收函数当参数】或【返回一个函数】的函数。
// map/filter/reduce/forEach 都是高阶函数;你传给它们的箭头函数就是回调。
// [1,2,3].map(n => n * 2)
// └─ map 是高阶函数 └─ n => n*2 是回调(map 会对每个元素调用它)
// 💡 这正是 agent/pi 的核心套路:你注册"工具函数",框架在合适时机回调它们。
//
// ── 1. map:每个元素做变换,返回【等长】新数组 ⭐⭐ ──
// const nums = [1, 2, 3]
// nums.map(n => n * 2) // [2, 4, 6] ← 一进一出,长度不变
// 回调的返回值 = 新数组对应位置的元素。不改原数组(返回新的)。
// 常用:从对象数组里"抽一列":
// users.map(u => u.name) // ['Tom', 'Jerry']
//
// ── 2. filter:挑出【回调返回 true】的元素 ⭐⭐ ──
// const nums = [1, 2, 3, 4]
// nums.filter(n => n % 2 === 0) // [2, 4] ← 回调返回 true 的才留下
// 回调要返回【布尔】(条件);返回 true 保留,false 丢弃。长度可能变短。不改原数组。
//
// ── 3. reduce:把整个数组【归并成一个值】⭐⭐(最难,最强)──
// nums.reduce((acc, n) => acc + n, 0)
// └acc └当前元素 └初始值
// • acc(accumulator 累加器):跨每一轮"攒结果"的容器,上一轮的返回值传给下一轮
// • 第二个参数 0 是 acc 的【初始值】(强烈建议总是写上)
// 过程(求和 [1,2,3],初始 0):
// 轮1 acc=0, n=1 → 返回 1
// 轮2 acc=1, n=2 → 返回 3
// 轮3 acc=3, n=3 → 返回 6 ← 最终结果
// reduce 能做 map/filter 能做的一切(求和、找最大、计数、拼接…),但先把上面三个用熟。
//
// ── 4. 其他常用高阶方法(顺带认识)──────────────
// forEach(fn) 只遍历做事,【没有返回值】(想要新数组用 map,别用 forEach)
// find(fn) 返回【第一个】让回调为 true 的元素(找不到 undefined)
// some(fn) 只要【有一个】满足 → true
// every(fn) 【全部】满足 → true
//
// ── 5. 链式调用(chaining)⭐ ────────────────────
// map/filter 返回新数组 → 可以接着点下一个方法,像流水线:
// nums.filter(n => n % 2 === 1).map(n => n * 10)
// 先挑出奇数,再各 ×10。真实代码里到处是这种"过滤→变换"的链。
//
// ═════ 要点回顾 ═════
// 1. 高阶函数=接收/返回函数;回调=被传进去、由别人调用的函数 ⭐
// 2. map:变换,等长新数组(回调返回值=新元素)⭐⭐
// 3. filter:筛选,回调返回布尔,true 才留 ⭐⭐
// 4. reduce:归并成一个值,(acc, n) => ...,别忘初始值 ⭐⭐
// 5. forEach 无返回值;find/some/every 各司其职
// 6. map/filter 返回新数组 → 可链式;都不改原数组 ⭐
//
// ═════════════════════ 示范 ═════════════════════
const nums = [1, 2, 3, 4, 5]
// ── 1. map:每个 ×2,等长新数组 ⭐ ─────────────────
console.log(nums.map((n) => n * 2)) // [ 2, 4, 6, 8, 10 ]
console.log(nums) // [ 1, 2, 3, 4, 5 ] ← 原数组没变
// ── 2. filter:只留偶数 ⭐ ─────────────────────────
console.log(nums.filter((n) => n % 2 === 0)) // [ 2, 4 ]
// ── 3. reduce:求和(初始值 0)⭐ ──────────────────
console.log(nums.reduce((acc, n) => acc + n, 0)) // 15
// 看清过程:把每一轮打印出来
nums.reduce((acc, n) => {
console.log(` acc=${acc}, n=${n}${acc + n}`) // 0,1→1 / 1,2→3 / 3,3→6 / 6,4→10 / 10,5→15
return acc + n
}, 0)
// ── 4. forEach:只做事,无返回值 ──────────────────
nums.forEach((n) => process.stdout.write(n + ' ')) // 1 2 3 4 5
console.log('← forEach 遍历')
console.log(nums.forEach((n) => n)) // undefined ← forEach 没有返回值!
// ── 5. find / some / every ───────────────────────
console.log(nums.find((n) => n > 3)) // 4 ← 第一个大于 3 的
console.log(nums.some((n) => n > 4)) // true ← 有一个大于 4
console.log(nums.every((n) => n > 0)) // true ← 全部大于 0
// ── 6. 链式:先筛奇数,再各 ×10 ⭐ ─────────────────
console.log(nums.filter((n) => n % 2 === 1).map((n) => n * 10)) // [ 10, 30, 50 ]
// ── 7. 对象数组(呼应 07 章,真实数据形状)⭐ ──────
const users = [
{ name: 'Tom', age: 20 },
{ name: 'Jerry', age: 18 },
]
console.log(users.map((u) => u.name)) // [ 'Tom', 'Jerry' ] ← 抽一列
console.log(users.filter((u) => u.age >= 20)) // [ { name: 'Tom', age: 20 } ] ← 挑成年
// ═════════════════════ 轮到你 ═════════════════════
// (只给要求,自己写代码)
// 练习 A(map):有 const prices = [10, 20, 30]
// 用 map 给每个价格打八折(×0.8),打印新数组([8, 16, 24])
const prices = [10, 20, 30]
console.log(prices.map(n => n * 0.8))
// 练习 B(filter):有 const ages = [12, 18, 25, 9, 30]
// 用 filter 挑出所有 >= 18 的,打印([18, 25, 30])
const ages = [12, 18, 25, 9, 30]
console.log(ages.filter(n => n >= 18))
// 练习 C(reduce):有 const nums = [3, 8, 1, 10, 5]
// 用 reduce 算出它们的总和,打印(27)
// 提示:回调是 (acc, n) => ...,初始值给 0
const nums_ = [3, 8, 1, 10, 5]
console.log(nums_.reduce((acc, n) => acc + n, 0))
// 练习 D(map + 对象数组):有下面这个 products
// const products = [
// { name: '苹果', price: 5 },
// { name: '香蕉', price: 3 },
// ]
// 用 map 得到一个【只含名字】的数组 ['苹果', '香蕉'],打印
const products = [
{ name: '苹果', price: 5 },
{ name: '香蕉', price: 3 },
]
console.log(products.map(product => product.name))
// 练习 E(链式,动脑):还是上面的 nums = [3, 8, 1, 10, 5]
// 先用 filter 挑出偶数,再用 map 把它们各自平方,打印。
// 提示:两个方法链着写,filter(...).map(...)
// ❌ ^ 在 JS 是"按位异或"不是乘方:8^2=10、10^2=8 → 得 [10, 8],和平方无关
// console.log(nums_.filter(n => n % 2 === 0).map(n => n^2))
// ✅ 乘方用 **(或 n*n)
console.log(nums_.filter(n => n % 2 === 0).map(n => n**2)) // [ 64, 100 ]
// 练习 F(reduce 进阶,挑战):有 const words = ['I', 'love', 'js']
// 用 reduce 把它们拼成一句话 'I love js'(单词间加空格)。
const words = ['I', 'love', 'js']
// ❌ 初始值给 "":第一轮 acc="" → " I",开头多一个空格 → 结果 " I love js"
// let sentence = words.reduce((acc, n) => `${acc} ${n}`, "")
// ❌ 想切掉开头空格却写错:sentence[1, ...] 的逗号是"逗号运算符",只取最后一个值,
// 等于 sentence[9] → 取【单个字符】's',不是切片(切片要用 .slice)
// console.log(sentence[1, sentence.length-1])
// 🔸 能救回来但不优雅:事后 slice 掉开头空格
// console.log(sentence.slice(1))
// ✅ 从源头解决:不给初始值 → reduce 拿第一个词当初始 acc、从第二个开始拼 → 开头无空格
const sentence = words.reduce((acc, n) => `${acc} ${n}`)
console.log(sentence) // I love js

@ -0,0 +1,104 @@
# 📚 我的 JavaScript 学习笔记
跟着 `30-Days-Of-JavaScript` 教程学,但**按知识体系整理**,方便日后复习。
开始于 2026-07。
---
## 🧭 怎么用这个目录
- 每个主题就是**根目录下一个 `编号-主题.js` 单文件**(如 `08-sets-maps.js`),
学练复习一体,内含三段:
1. **概念总结区**(文件顶部注释,含 ASCII 表格)—— 复习先看这里
2. **示范区** —— 带预期输出的可运行代码
3. **轮到你区** —— 练习题,只给要求不给答案
`// ═════ 概念总结 ═════` / `// ═════ 示范 ═════` / `// ═════ 轮到你 ═════` 分隔。
- 编号(`01-`, `02-`…)只表示**推荐学习顺序**,不是"第几天"
- 例外:`00-concepts/` 仍是文件夹(跨主题通用概念一组,多为 `.md`)
> 📌 结构演变:早期用 `NN-topic/`(内含 `notes.md`+`practice.js`)分开两文件,
> 现已扁平化为**根目录单文件 `NN-topic.js`**(概念写进代码注释),减少重复、复习更快。
### 运行代码的三种方式
| 方式 | 怎么做 | 适合 |
|---|---|---|
| **Quokka(免费版)** | `Cmd+Shift+P``Quokka: Start on Current File` | 边写边看 `console.log` 的值(纯逻辑练习) |
| **Code Runner ▷** | 右上角 ▷,或 `Ctrl+Alt+N` | 在终端跑整个文件 |
| **浏览器 + F12** | 打开 `index.html`,看 Console | 涉及 `document`/DOM 的代码 |
> ⚠️ 判断能不能用 ▷/Quokka 跑:文件**自包含**(自己定义自己用、无 `document`)就能跑;
> 依赖别的文件的变量、或含 `document` 的,要用**浏览器**。
---
## 🗺️ 知识地图(带进度)
### 〇、通用概念(跨主题,随时查)
- `00-concepts/my-mistakes.md` —— 🕳️ **我的踩坑记录**(复习先看这个!含报错秒查表)⭐
- `00-concepts/call-forms.md` —— 三种调用形式:`值.方法()` vs `函数()` vs `类.方法()`
- `00-concepts/value-vs-reference.md` —— 值 vs 引用:复制 vs 共享、字符串不可变 ⭐
- `00-concepts/shortcircuit-bash-vs-js.md` —— `&&`/`||`:bash vs JS(`0` 真假相反)
- `00-concepts/semicolons-and-asi.md` —— 分号 `;` 什么时候【必须】写(ASI 的坑:行首 `(`/`[`、`return` 换行)⭐
- `00-concepts/expression-vs-statement.md` —— 表达式 vs 语句:谁有值/能当值用(串起箭头 `=>`、三元、return)⭐
### 一、基础与数据
- [x] `01-basics` 基础:输出、注释、表达式、字符串引号、运行方式
- [x] `02-variables` 变量与数据类型
- [x] `03-operators` 运算符
- [x] `04-strings` 字符串(常用方法)
- [x] `05-conditionals` 条件与布尔
### 二、数据结构
- [x] `06-arrays` 数组
- [x] `07-objects` 对象
- [x] `08-sets-maps` Set 与 Map
> 🎯 **本路线已为目标「用 pi 搭 agent + 读懂 pi 关键源码」做过适配**:
> 异步大幅提前(agent 命脉)、新增模块与 TS 专题、前端相关暂缓。原教程的
> day 顺序仅作参考。
### 三、逻辑与函数(pi 前置 · 核心)
- [x] `09-loops` 循环
- [x] `10-functions` 函数(声明/表达式、箭头、参数、返回、作用域、提升/TDZ)
- [x] `11-higher-order-functions` 高阶函数 + 回调(map / filter / reduce、链式)← 读 TS 必备
- [ ] `12-destructuring-spread` 解构与展开 ← pi 源码里到处是,读代码前置
- [ ] `13-closures` 闭包(理解 agent 状态、回调捕获)
### 四、异步与工程结构(agent 的命脉)⭐
- [ ] `14-modules` 模块 import / export ← 新增:monorepo/多包的地基
- [ ] `15-promises-async` 异步 Promise / async-await ← 从原第18提前!每个 LLM 调用都是异步
- [ ] `16-error-handling` 错误处理 try/catch ← 和 await 搭配
- [ ] `17-classes` 类 class ← agent 运行时/状态管理常用
- [ ] `18-json` JSON ← LLM 的输入输出、tool calling 都是 JSON
### 五、TypeScript(看懂 pi 的门票)⭐
- [ ] `19-ts-basics` TS 基础:为什么要类型、类型标注、`interface` / `type`
- [ ] `20-ts-generics` 泛型 `<T>`、联合类型、如何读懂函数签名
- [ ] `21-ts-project` 工程:模块类型、`tsconfig`、怎么读 `.d.ts` 和别人的类型
### 六、Agent 领域 + pi 实战
- [ ] `22-agent-concepts` 领域概念:LLM API、tool calling、agent loop 是什么
- [ ] `23-pi-hello` 用 pi 搭一个最小可跑的 agent
- [ ] `24-pi-source-reading` 精读 pi 源码(挑软柿子,逐行带读,不求全懂)
### 附:暂缓(和 pi 无关,想做前端再回来)
- 正则表达式 / DOM 操作 / 事件监听 / Web Storage / 综合前端项目
---
## 📏 练习规矩(给协作者/AI)
- **练习题让用户自己改**,不要替他写答案。
- **出题不写答案**:"轮到你"区域只给要求,不放正确答案(哪怕注释掉也不行,会剧透)。
- **提示只给方向,不给完整写法**:如 `raw.trim().toLowerCase()` 这种整句提示等于给答案,不行;
应改成"两个方法可以链式连写"这种点思路的说法。
- 示范区(讲解用的例子)可带预期输出;题目区不带。
- **用户每犯一个新错,吸收进 `00-concepts/my-mistakes.md`**(错法→现象→正解→教训),
并带 **📍来源索引**(原题文件+练习号、相关笔记章节),方便跳回原题。复习时才记得住坑在哪。
## ✍️ 命名约定
- 文件夹:`编号-英文主题`,如 `06-arrays`
- 笔记:`notes.md`;练习:`practice.js`
- 一个主题内容多时,练习可拆成 `practice_01_xxx.js`

@ -0,0 +1,25 @@
// 🐿️ Quokka 试验场(免费版写法)
// 免费版不支持 //? ,但支持实时 console.log —— 值会显示在行尾
// 用法:Cmd+Shift+P → "Quokka: Start on Current File"
// 然后随便改数字/代码,右边的值会实时变化,不用保存、不用切终端
// 1) 基本变量
const a = 2
const b = 3
console.log(a + b) // 实时显示 5
// 2) 字符串
const name = 'JavaScript'
console.log(name.length) // 10
console.log(name.toUpperCase()) // 'JAVASCRIPT'
// 3) 数组方法(练前 20 天最常玩的东西)
const nums = [1, 2, 3, 4, 5]
console.log(nums.map((n) => n * 2)) // [2, 4, 6, 8, 10]
console.log(nums.filter((n) => n % 2 === 0)) // [2, 4]
console.log(nums.reduce((sum, n) => sum + n, 0)) // 15
// 4) 试试改这里的值,看右边跟着变 👇
let radius = 5
const area = Math.PI * radius ** 2
console.log(area) // 改 radius 试试,这个值会实时跟着变
Loading…
Cancel
Save