diff --git a/.claude/skills/learn-tidy/SKILL.md b/.claude/skills/learn-tidy/SKILL.md new file mode 100644 index 000000000..0bca2750d --- /dev/null +++ b/.claude/skills/learn-tidy/SKILL.md @@ -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」「收拾干净」 +- 用户刚做完一章练习(A–F 之类)、或刚改完一段 `// 试验 / 探索` 代码 +- 文件里留下了**对错混杂的多版尝试**(比如 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。别长篇复述文件内容——用户会自己翻文件。 diff --git a/11_Day_Destructuring_and_spreading/11_day_destructuring_and_spreading.md b/11_Day_Destructuring_and_spreading/11_day_destructuring_and_spreading.md index d4ffc343f..c538365af 100644 --- a/11_Day_Destructuring_and_spreading/11_day_destructuring_and_spreading.md +++ b/11_Day_Destructuring_and_spreading/11_day_destructuring_and_spreading.md @@ -93,7 +93,7 @@ console.log(backEnd) ["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 const numbers = [1, 2, 3] @@ -184,7 +184,7 @@ Node Express MongoDB ### 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 const rectangle = { @@ -258,7 +258,6 @@ const calculatePerimeter = rectangle => { } console.log(calculatePerimeter(rect)) // 60 -//with destructuring ``` ```js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..65b6f70a6 --- /dev/null +++ b/CLAUDE.md @@ -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 风格(`(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 是**公开的** —— 不要提交任何 +私密内容。 diff --git a/js_notes/00-concepts/call-forms.md b/js_notes/00-concepts/call-forms.md new file mode 100644 index 000000000..136a7d2b1 --- /dev/null +++ b/js_notes/00-concepts/call-forms.md @@ -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 帮你查最快。 diff --git a/js_notes/00-concepts/expression-vs-statement.md b/js_notes/00-concepts/expression-vs-statement.md new file mode 100644 index 000000000..88b5cec87 --- /dev/null +++ b/js_notes/00-concepts/expression-vs-statement.md @@ -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 在显示"完成值",别当真。 diff --git a/js_notes/00-concepts/my-mistakes.md b/js_notes/00-concepts/my-mistakes.md new file mode 100644 index 000000000..778dca3bb --- /dev/null +++ b/js_notes/00-concepts/my-mistakes.md @@ -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 | diff --git a/js_notes/00-concepts/semicolons-and-asi.md b/js_notes/00-concepts/semicolons-and-asi.md new file mode 100644 index 000000000..c3c64c81a --- /dev/null +++ b/js_notes/00-concepts/semicolons-and-asi.md @@ -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 项目社区**更偏爱每句都写 `;`** —— 那是团队约定,不是语法强制。 diff --git a/js_notes/00-concepts/shortcircuit-bash-vs-js.md b/js_notes/00-concepts/shortcircuit-bash-vs-js.md new file mode 100644 index 000000000..d182e0bed --- /dev/null +++ b/js_notes/00-concepts/shortcircuit-bash-vs-js.md @@ -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 是假**。 diff --git a/js_notes/00-concepts/value-vs-reference.md b/js_notes/00-concepts/value-vs-reference.md new file mode 100644 index 000000000..7533dc01b --- /dev/null +++ b/js_notes/00-concepts/value-vs-reference.md @@ -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]` 通向本体。** diff --git a/js_notes/01-basics.js b/js_notes/01-basics.js new file mode 100644 index 000000000..eda67030a --- /dev/null +++ b/js_notes/01-basics.js @@ -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) diff --git a/js_notes/02-variables.js b/js_notes/02-variables.js new file mode 100644 index 000000000..683c768c1 --- /dev/null +++ b/js_notes/02-variables.js @@ -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) diff --git a/js_notes/03-operators.js b/js_notes/03-operators.js new file mode 100644 index 000000000..5cde38d04 --- /dev/null +++ b/js_notes/03-operators.js @@ -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 + diff --git a/js_notes/04-strings.js b/js_notes/04-strings.js new file mode 100644 index 000000000..154ae47cd --- /dev/null +++ b/js_notes/04-strings.js @@ -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)) diff --git a/js_notes/05-conditionals.js b/js_notes/05-conditionals.js new file mode 100644 index 000000000..afbeae2d6 --- /dev/null +++ b/js_notes/05-conditionals.js @@ -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') +} diff --git a/js_notes/06-arrays.js b/js_notes/06-arrays.js new file mode 100644 index 000000000..eeea15755 --- /dev/null +++ b/js_notes/06-arrays.js @@ -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('-')) \ No newline at end of file diff --git a/js_notes/07-objects.js b/js_notes/07-objects.js new file mode 100644 index 000000000..9630725e4 --- /dev/null +++ b/js_notes/07-objects.js @@ -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) \ No newline at end of file diff --git a/js_notes/08-sets-maps.js b/js_notes/08-sets-maps.js new file mode 100644 index 000000000..7a7046e7a --- /dev/null +++ b/js_notes/08-sets-maps.js @@ -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() diff --git a/js_notes/09-loops.js b/js_notes/09-loops.js new file mode 100644 index 000000000..5bbcac085 --- /dev/null +++ b/js_notes/09-loops.js @@ -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) +} diff --git a/js_notes/10-functions.js b/js_notes/10-functions.js new file mode 100644 index 000000000..f978c8c1b --- /dev/null +++ b/js_notes/10-functions.js @@ -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, ... } diff --git a/js_notes/11-higher-order-functions.js b/js_notes/11-higher-order-functions.js new file mode 100644 index 000000000..4913f20bc --- /dev/null +++ b/js_notes/11-higher-order-functions.js @@ -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 \ No newline at end of file diff --git a/js_notes/README.md b/js_notes/README.md new file mode 100644 index 000000000..7c3cacaf1 --- /dev/null +++ b/js_notes/README.md @@ -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` 泛型 ``、联合类型、如何读懂函数签名 +- [ ] `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` 等 diff --git a/js_notes/quokka_playground.js b/js_notes/quokka_playground.js new file mode 100644 index 000000000..d44af1100 --- /dev/null +++ b/js_notes/quokka_playground.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 试试,这个值会实时跟着变