Merge 0c830c906e into e067490424
commit
6ec242da8f
@ -0,0 +1,169 @@
|
||||
# CLAUDE.md
|
||||
|
||||
本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。
|
||||
|
||||
## 这个仓库到底是什么
|
||||
|
||||
这里并存着两层内容:
|
||||
|
||||
1. **上游教程** —— `NN_Day_*` 文件夹(例如 `01_Day_Introduction`、
|
||||
`23_Day_Event_listeners`),以及各语言翻译文件夹(`Korea/`、`RU/`、
|
||||
`Spanish/`……)。这是 Asabeneh 的 "30 Days Of JavaScript" 课程,作为**只读**
|
||||
参考资料克隆进来。**不要修改或"修复"这些文件** —— 其中的错误(例如 `main.js`
|
||||
依赖另一个单独加载的 `variable.js` 里的变量)是刻意为之的教学产物,不是 bug。
|
||||
|
||||
2. **`js_notes/`** —— 用户自己的学习笔记,也是**活跃工作区**。
|
||||
几乎所有真正的工作都发生在这里。它**按知识主题组织,而非按天**
|
||||
(`01-basics`、`02-variables`、……`07-objects`……),因为这个编号反映的是
|
||||
用户的学习顺序,而不是教程的天数。
|
||||
|
||||
用户是**正在入门 JavaScript 的初学者**(有一些 Python/Linux 背景)。
|
||||
每次会话都是交互式辅导:讲一个主题、让用户练习、复审他的代码。
|
||||
|
||||
## `js_notes/` 的结构与约定
|
||||
|
||||
- **一个主题一个文件**,平铺在 `js_notes/` 根目录:`NN-topic.js`(例如
|
||||
`09-loops.js`)。每个文件都自成一体,包含"学习 + 练习 + 复习",分为三个
|
||||
带标签的区块,顺序如下:
|
||||
1. `// ═════ 概念总结 ═════` —— 以注释形式呈现概念(含对齐的 ASCII 表格)
|
||||
2. `// ═════ 示范 ═════` —— 可运行的演示代码,可带预期输出注释
|
||||
3. `// ═════ 轮到你 ═════` —— 练习:只给要求,由用户自己写代码
|
||||
`js_notes/README.md` 是索引 + 进度地图。
|
||||
- **历史沿革:** 主题过去是 `NN-topic/` 文件夹,里面放 `notes.md` +
|
||||
`practice.js`;后来被扁平化为单个 `NN-topic.js` 文件。`my-mistakes.md` 里有些
|
||||
`📍来源` 链接仍指向旧的 `../NN-topic/practice.js` 路径 —— 请把它们视作
|
||||
`../NN-topic.js`(只在你本来就在编辑那条条目时才顺手修链接;不要对用户的文件
|
||||
做一次性的全量重写)。
|
||||
- `js_notes/00-concepts/` 保持为一个**文件夹**,存放跨主题的 `.md` 笔记
|
||||
(值 vs 引用、调用形式、bash-vs-js 短路、分号与 ASI),外加
|
||||
**`my-mistakes.md`** —— 一份用户实际犯过的错误的滚动记录,每条采用
|
||||
错法→现象→正解→教训 格式,并带一个指回原练习的 `📍来源` 反向引用。
|
||||
|
||||
**`js_notes/README.md` 含有具有约束力的辅导规则 —— 教学前先读它。**
|
||||
其中最关键的几条:
|
||||
|
||||
- **不替用户写答案。** 在练习文件里,"轮到你"区块只给要求 —— 不给解答,
|
||||
连注释掉的解答也不行(那会破坏练习)。提示只指方向("这两个方法可以链式
|
||||
调用"),不轻易给出完整的一整行,比如 `raw.trim().toLowerCase()`。
|
||||
- **让用户自己修自己的代码。** 指出哪里错了、为什么错;不要替他改练习答案。
|
||||
- 演示区块(教学示例)可以用注释写出预期输出;练习区块不可以。
|
||||
- **当用户犯了新错误时,把它吸收进 `00-concepts/my-mistakes.md`**
|
||||
(错法→现象→正解→教训 格式,带 `📍来源` 索引);或者如果不值当单开一条,就直接在那个笔记里 Edit 也行,这样复习时可追溯。
|
||||
但**只记真正有教益的错误** —— 已被现有条目覆盖的纯手误,或用户已经自己
|
||||
修好、没有新东西可学的错误,不值得新开一条(记流水账的话用户会反驳)。
|
||||
拿不准时,问一下。
|
||||
- 一个主题完成后,更新 `js_notes/README.md` 里的进度勾选框。
|
||||
|
||||
### 一章收尾必做清单(用户的长期要求)⭐
|
||||
|
||||
当一章的练习全部做完、确认无误时,**每次都要走完这套收尾流程**(别遗漏、别偷懒):
|
||||
|
||||
1. **跑一遍验证**:`node js_notes/NN-topic.js` 确认整章(含用户练习答案)能跑、
|
||||
输出符合预期;必要时 `node --check` 先查语法。
|
||||
2. **留学习痕迹**:用户练习中犯过的错,按"改用户代码时留学习痕迹"那条,在文件里
|
||||
保留 `❌/✅` 对照(绝不留能跑的坏代码)。
|
||||
3. **吸收错误**:本章冒出的、有教益的新错误,按上面规则并入 `my-mistakes.md`
|
||||
(值当就新开条目、不值当就在相关条目补一句;纯手误/已覆盖的不记)。
|
||||
4. **更新进度**:勾选 `js_notes/README.md` 里该章的进度框(`[ ]` → `[x]`),
|
||||
必要时补上本章实际覆盖到的关键词。
|
||||
5. **提交**:按逻辑拆成干净的 commit 提交(Angular 风格 message);工作区留干净。
|
||||
6. **导向下一步**:简述下一章要学什么、和已学内容/pi 目标的关联,让用户决定是否继续。
|
||||
|
||||
### 改用户代码时留下学习痕迹(用户的长期要求)
|
||||
|
||||
每当用户让你改他的代码或加标记时,**把错误版本作为注释掉的 `❌ / ✅` 对照
|
||||
保留**在修复的旁边 —— 好让未来的他看到*错在哪、为什么错*,而不仅仅是改对后的
|
||||
那一行。这与用户已经在 `06-arrays.js` / `07-objects.js` 里用的风格一致:
|
||||
|
||||
```js
|
||||
// ❌ splice 返回的是"被删除的元素",不是修改后的数组
|
||||
// console.log(q.splice(1, 0, 'second'))
|
||||
// ✅
|
||||
q.splice(1, 0, 'second')
|
||||
console.log(q)
|
||||
```
|
||||
|
||||
这类痕迹的规则:
|
||||
- 把错误行注释掉(绝不留下能跑的坏代码),放在修复的**上方**。
|
||||
- 一行简短的 `// ❌ 原因` 说明*为什么*错;`// ✅` 标出正确的代码。
|
||||
- 这适用于**演示/教学区块,以及用户明确要求的修复** ——
|
||||
它**不**推翻"绝不写答案":对于用户还没动手的 `轮到你` 练习,仍然不要
|
||||
往里丢解答。
|
||||
- 由此浮现出的、有分量的新陷阱,通常也应加一条 `my-mistakes.md` 条目
|
||||
(以上文"只记有教益的"这条为准)。
|
||||
|
||||
### 讲清*为什么*,而不只是规则(用户的长期要求)
|
||||
|
||||
比起干巴巴的"就是这么规定的",用户从底层机制里学得快得多。只给规则、
|
||||
不给原因的简略笔记是不够的 —— 用户明确指出 `// 带 {} 就得自己写 return`
|
||||
太晦涩。当一条规则有其原因时,**先讲原因**;机制一旦讲清,规则就不言自明
|
||||
(而且往往好几条规则会归结到同一个原因)。
|
||||
|
||||
举例说明 —— 箭头函数的函数体与 `return`。**先说出那句最核心的话**,再让
|
||||
一切作为它的推论自然而然地推出(能找到那句核心才是本事 —— 用户曾修正过一版
|
||||
从 `{` 的两种身份出发、而非从那句核心出发的草稿):
|
||||
- **核心一句:`=>` 的右侧期待一个*表达式***(某个求值后得到一个值的东西)。
|
||||
- 给它一个表达式 → 那*就是*那个值 → JS 把它返回(隐式返回)。
|
||||
在那里写 `return` 是非法的:`return` 是*语句*,不是表达式,所以
|
||||
`(a, b) => return a + b` 会 `SyntaxError`。
|
||||
- `{ ... }` 是逃生舱:以 `{` 开头的函数体会让 JS 把它当作*语句块*,
|
||||
语句块不产生值 → 你必须显式 `return`,否则得到 `undefined`。
|
||||
- 由这同一句核心推出:对象字面量也以 `{` 开头,于是和语句块规则冲突 →
|
||||
用括号包起来以强制进入表达式上下文:`(x) => ({ name: x })`。
|
||||
|
||||
由此得出的实操习惯:
|
||||
- 优先说"X **因为**解析器/引擎看到了 Y",而不是"总是这样做 X"。
|
||||
- **用运行代码来验证**,当它能让论点更锋利时 —— 用户看重看到真实的报错
|
||||
信息或输出,胜过一句断言(例如用 `node --check` 显示一个真实的
|
||||
`SyntaxError`)。
|
||||
|
||||
### 讲解【直接写进文件】,别只留在聊天里(用户的长期要求)⭐
|
||||
|
||||
用户反馈:讲解如果只写在聊天回复里,事后几乎不会再翻出来;而临时验证文件
|
||||
(`/tmp/*.js`)他更是看不到。所以**成品是文件,不是聊天**。当一段讲解值得
|
||||
留存时:
|
||||
- **直接写进对应的 `NN-topic.js`**(概念区讲原理、示范区放可跑代码),
|
||||
而不是"先在聊天里长篇讲一遍、再写一遍文件" —— 那样既浪费上下文,产物
|
||||
用户又拿不到。聊天里只做**极简导读 + 指向文件哪一节**。
|
||||
- 临时用 `/tmp/*.js` 跑验证没问题,但**结论要落进文件**;别把只存在于
|
||||
`/tmp` 或聊天里的解释当成交付。
|
||||
- 跨主题的点,写进该去的那一章,并在相关章节挂一句呼应(例:提升 hoisting
|
||||
写进 10-functions.js,在 02-variables.js 的 var 那格挂一句指过去)。
|
||||
- 这条和上面"讲清为什么""留 ❌/✅ 痕迹"是一套:原理讲清 + 落进文件 +
|
||||
可追溯,才是一次完整的讲解。
|
||||
|
||||
## 运行代码
|
||||
|
||||
这里**没有构建系统、没有 package.json、没有 linter、没有测试套件** ——
|
||||
这是一个学习仓库,不是一个应用。代码逐个文件运行:
|
||||
|
||||
```bash
|
||||
node js_notes/06-arrays.js # 运行单个主题文件
|
||||
node --check js_notes/09-loops.js # 只做语法检查,不执行
|
||||
```
|
||||
|
||||
在编辑器里,用户也会用 **Quokka.js**(内联显示实时 `console.log` 值;免费版 ——
|
||||
不支持 `//?` 实时注释)或 **Code Runner**(▷ 按钮)来运行文件。
|
||||
|
||||
**自足文件 vs 浏览器文件:** 一个 `.js` 文件如果自己定义了它用到的一切、
|
||||
且不碰 `document`,就能在 Node/Quokka/Code Runner 下运行。而依赖另一个脚本里
|
||||
变量的文件,或使用 `document`/DOM 的文件(第 21 天以后以及那些小项目),
|
||||
必须通过它们的 `index.html` 在浏览器里打开、配合 F12 控制台 —— 在 Node 下
|
||||
运行会抛错(例如 `firstName is not defined`、`document is not defined`)。
|
||||
|
||||
## 内容语言
|
||||
|
||||
散文、笔记、讲解用**中文**;代码、标识符、commit message 用英文。
|
||||
Commit message 遵循 Angular 风格(`<type>(scope): description`)。
|
||||
|
||||
## Git 远端
|
||||
|
||||
这个克隆是从原课程 fork 出来的:
|
||||
|
||||
- `origin` → `github.com/JaydonZhao/30-Days-Of-JavaScript`(用户的 fork;**推到这里**)
|
||||
- `upstream` → `github.com/Asabeneh/30-Days-Of-JavaScript`(原作者;拉取教程更新用
|
||||
`git fetch upstream && git merge upstream/master` —— 用 merge,不用 rebase)
|
||||
|
||||
工作直接发生在 `master` 上(这个 fork 的 `master` 独立于 upstream 的,所以
|
||||
用户的提交永远不会碰到别人的仓库)。这个 fork 是**公开的** —— 不要提交任何
|
||||
私密内容。
|
||||
@ -0,0 +1,67 @@
|
||||
# 通用概念 · 三种调用形式:`值.方法()` vs `函数()` vs `类.方法()`
|
||||
|
||||
> 跨主题概念,不属于某一天。困惑"什么时候用点、什么时候传参"时看这篇。
|
||||
|
||||
## 核心问题:这个函数"属于"谁?
|
||||
|
||||
### 形式一:`值.方法()` —— 方法(method)
|
||||
函数**属于某个值/对象**,是它"自带的能力",用点 `.` 调用。
|
||||
**点前面那个值 = 被操作的主角**,不用再传进去。
|
||||
|
||||
```js
|
||||
'hello'.toUpperCase() // toUpperCase 是字符串自带的能力
|
||||
;[1, 2, 3].map((x) => x * 2) // map 是数组自带的能力
|
||||
'a,b'.split(',') // split 属于字符串
|
||||
```
|
||||
|
||||
### 形式二:`函数(值)` —— 独立函数(function)
|
||||
函数**不属于任何特定值**,是自由的工具,数据当参数**传进括号**。
|
||||
|
||||
```js
|
||||
Number('42') // 转谁就传谁进去
|
||||
parseInt('42px')
|
||||
isNaN(x)
|
||||
```
|
||||
|
||||
### 形式三:`类.方法(值)` —— 静态方法(static method)
|
||||
函数挂在某个**类/工具箱**下做归类,数据仍然**传进去**。
|
||||
|
||||
```js
|
||||
Array.isArray([1, 2, 3]) // isArray 挂在 Array 名下
|
||||
Math.max(1, 2, 3) // max 挂在 Math 名下
|
||||
Number.isNaN(x) // 挂在 Number 名下
|
||||
Object.keys(obj) // 挂在 Object 名下
|
||||
```
|
||||
|
||||
> ⚠️ 区分:`Array.isArray(arr)` 的 `Array`(大写=类/工具箱)和 `[1,2,3].map()` 点前的
|
||||
> 数组实例(具体的值)是两回事。**大写开头 = 工具箱;点前是具体值 = 实例。**
|
||||
|
||||
## 对照表
|
||||
|
||||
| 形式 | 例子 | 数据在哪 | 感觉 |
|
||||
|---|---|---|---|
|
||||
| `值.方法()` | `'hi'.toUpperCase()` | **点前面** | 值自带的能力 |
|
||||
| `函数(值)` | `Number('42')` | **括号里** | 自由工具 |
|
||||
| `类.方法(值)` | `Array.isArray(x)` | **括号里** | 工具箱里的工具 |
|
||||
|
||||
## "什么时候用哪个"?
|
||||
|
||||
**真相:不是你选的,是语言设计时定死的。** 每个功能造出来就固定了形式,只能记/查。
|
||||
- 转大写只能 `str.toUpperCase()`,不能 `toUpperCase(str)`
|
||||
- 判断数组只能 `Array.isArray(x)`,不能 `x.isArray()`
|
||||
|
||||
但有规律可猜:
|
||||
1. **操作字符串/数组/对象内容的** → 大多 `值.方法()`
|
||||
2. **类型转换/数学/解析类工具** → 大多 `函数()` 或 `类.方法()`(`Number`、`Math.`、`JSON.parse`)
|
||||
3. **`Math.` / `JSON.` / `Object.` / `Array.` 开头** → 几乎都是静态方法,数据传进去
|
||||
|
||||
## 记忆诀窍
|
||||
|
||||
> 问:**"这是某个值天生就会的,还是外部工具对它做的?"**
|
||||
> - 天生就会(大写化、截取、拆分)→ `值.方法()`
|
||||
> - 外部工具处理它(转类型、算最值、解析)→ `函数(值)` / `工具箱.方法(值)`
|
||||
|
||||
## 🔧 实用技巧
|
||||
|
||||
**不确定时,在编辑器里敲 `值.` 看自动补全弹什么** —— 弹出来的就是它自带的方法;
|
||||
没弹出来的,就得找独立函数或静态方法。让 VS Code 帮你查最快。
|
||||
@ -0,0 +1,53 @@
|
||||
# 通用概念 · 表达式(expression) vs 语句(statement)⭐
|
||||
|
||||
> 跨主题核心区分。它决定"这东西能不能当值用",串起箭头函数、return、三元、if。
|
||||
> 相关:`../10-functions.js` §2(箭头 => 期待表达式)、`../05-conditionals.js`(三元 vs if)。
|
||||
|
||||
## 一句话分界
|
||||
|
||||
- **表达式 = 能算出一个【值】的东西** → 有值,能赋给变量、能当参数传。
|
||||
- **语句 = 执行一个【动作】的东西** → 在你的代码里【没有值】,不能当值用。
|
||||
|
||||
判断诀窍:**能不能塞进 `=` 右边(或当函数参数)?能 = 表达式;塞进去报 SyntaxError = 语句。**
|
||||
|
||||
## 谁是表达式,谁是语句
|
||||
|
||||
| 表达式(有值) | 语句(没值) |
|
||||
|---|---|
|
||||
| `1 + 2`、`n * 2` | `if (...) {...}` |
|
||||
| `'hi'`、`` `${x}` `` | `for / while (...) {...}` |
|
||||
| `x = 99`(赋值也是表达式!值是 99) | `let x = 1` / `const y = 2`(声明) |
|
||||
| `5 > 3 ? 'a' : 'b'`(三元) | `return x` / `break` / `continue` |
|
||||
| `fn(1, 2)`(函数调用) | `function f(){}`(函数声明) |
|
||||
|
||||
```js
|
||||
const a = 1 + 2 // ✅ 1+2 是表达式,有值 3
|
||||
const b = if (true) { 1 } // ❌ SyntaxError:if 是语句,没有值,不能赋
|
||||
const m = 5 > 3 ? 'a' : 'b' // ✅ 三元是表达式;换成 if 就不行 → 这就是三元存在的意义
|
||||
```
|
||||
|
||||
## ⚠️ 最容易被骗的地方:控制台里"语句好像有值"
|
||||
|
||||
在浏览器 F12 控制台 / `eval` 里敲一句 `if (true) { 5 }`,回车会显示 `5`,看着"像有返回值":
|
||||
|
||||
```js
|
||||
eval('if (true) { 5 }') // 5 ← 底层的"完成值(completion value)"
|
||||
eval('let z = 1') // undefined
|
||||
const bad = if (true) {5} // ❌ SyntaxError:想把它捞进变量,立刻现原形
|
||||
```
|
||||
|
||||
真相:语句在**引擎底层**确实有个"完成值",但**你在正常代码里拿不到**,只有
|
||||
`eval` 和 REPL(控制台)会把它显示出来。**别被控制台的显示骗成"语句能当值用"。**
|
||||
|
||||
## 为什么这个区分重要(它串起好几个点)⭐
|
||||
|
||||
- **箭头函数**:`=>` 右边【期待表达式】。所以 `(a,b) => a+b` ✅(a+b 是表达式);
|
||||
`(a,b) => return a+b` ❌ SyntaxError —— 因为 `return` 是【语句】,没有值,不能放这。
|
||||
- **三元 vs if**:三元是表达式(能 `const x = 条件 ? a : b`),if 是语句(不能赋值)。
|
||||
需要"按条件得到一个值"时用三元,正因为它有值。
|
||||
- **隐式返回**:箭头后给表达式 → 它就是值 → 自动返回;给 `{语句块}` → 块没值 → 得手写 return。
|
||||
|
||||
## ✅ 一句话记死
|
||||
|
||||
> **表达式有值、能当值用(赋值/传参);语句是动作、在代码里没值(塞进 `=` 右边就 SyntaxError)。**
|
||||
> 控制台里语句"显示出一个值"是 REPL 在显示"完成值",别当真。
|
||||
@ -0,0 +1,201 @@
|
||||
# 🕳️ 我的踩坑记录
|
||||
|
||||
> 这里记录我**自己真实犯过**的错(不是泛泛知识点)。复习时先扫这里 —— 我最容易在这些地方翻车。
|
||||
> 每条格式:**错法 → 现象 → 正解 → 教训**,并标注**📍来源**(点开可回到原题)。
|
||||
|
||||
---
|
||||
|
||||
## A. 方法忘加括号
|
||||
|
||||
📍来源:`../04-strings/practice.js` 练习 A(`../04-strings/notes.md` §1)
|
||||
```js
|
||||
city.toUpperCase // ❌ 打印出 [Function: toUpperCase],不是结果
|
||||
city.toUpperCase() // ✅ 加 () 才执行
|
||||
```
|
||||
- **现象**:打印出一堆 `[Function: xxx]` 而不是想要的值
|
||||
- **教训**:**方法是"动作",必须加 `()`**;`.length` 是属性不加括号。分不清就问:这是"数据"还是"动作"?
|
||||
|
||||
## B. 模板字符串用错符号
|
||||
|
||||
📍来源:`../02-variables/practice.js` 第 6 组(`../01-basics/notes.md` §4)
|
||||
```js
|
||||
`我叫 &{myName}` // ❌ 用了 &,原样打印 "我叫 &{myName}"
|
||||
`我叫 ${myName}` // ✅ 是 $ 不是 &
|
||||
```
|
||||
- **教训**:模板插值是 **`$` + `{}`**,不是 `&`。且只在反引号 `` ` `` 里生效。
|
||||
|
||||
## C. 方法名拼写错
|
||||
|
||||
📍来源:`../04-strings/practice.js` 练习 B
|
||||
```js
|
||||
email.spilt('@') // ❌ 报错:email.spilt is not a function
|
||||
email.split('@') // ✅
|
||||
```
|
||||
- **现象**:`X is not a function`
|
||||
- **教训**:看到 `is not a function`,先检查**拼写**。
|
||||
|
||||
## D. "能算 ≠ 能看见"(反复犯)
|
||||
|
||||
📍来源:`../04-strings/practice.js` 练习 E、`../06-arrays/practice.js` 练习 E(`../01-basics/notes.md` §5)
|
||||
```js
|
||||
array.join('-') // ❌ 算出来了但没打印,屏幕啥都没有
|
||||
console.log(array.join('-')) // ✅
|
||||
```
|
||||
- **教训**:文件里**必须 `console.log`** 才看得到结果。裸表达式会算但不显示。
|
||||
|
||||
## E. 方法返回值没接住(反复犯)⭐
|
||||
|
||||
📍来源:`../04-strings/practice.js` 练习 D/E、`../06-arrays/practice.js` 练习 E
|
||||
```js
|
||||
sentence.split(' ') // ❌ 拆了但没接住,等于白拆
|
||||
const words = sentence.split(' ') // ✅ 用变量接住
|
||||
```
|
||||
- **教训**:`slice / split / join / map / toUpperCase...` 都**返回新值、不改原**,必须用变量接住或直接打印。
|
||||
|
||||
## F. 改原 vs 返回新,分不清 ⭐⭐
|
||||
|
||||
📍来源:`../06-arrays/practice.js` 练习 D/E(`../06-arrays/notes.md` §4-5)
|
||||
|
||||
- **改原数组**:`push / pop / shift / unshift / splice`
|
||||
- **返回新值(不改原)**:`slice / split / join / map / filter`
|
||||
- `splice` 尤其坑:**返回的是"被删元素",不是修改后的数组** → 插入后要打印**原数组**,别打印返回值
|
||||
```js
|
||||
q.splice(1, 0, 'x') // 改的是 q 本身
|
||||
console.log(q) // ✅ 打印原数组
|
||||
console.log(q.splice(...)) // ❌ 打印的是被删元素([])
|
||||
```
|
||||
|
||||
## G. slice 用 length-1 丢了最后一个字符
|
||||
|
||||
📍来源:`../04-strings/practice.js` 练习 D(`../04-strings/notes.md` §4)
|
||||
```js
|
||||
name.slice(1, name.length - 1) // ❌ -1 少取一位
|
||||
name.slice(1) // ✅ 省略 end 取到结尾
|
||||
```
|
||||
- **教训**:`slice` **不含 end**;要取到结尾直接省略第二个参数。
|
||||
|
||||
## H. 变量没声明就赋值
|
||||
|
||||
📍来源:`../06-arrays/practice.js` 练习 E(`../02-variables/notes.md` §2-3)
|
||||
```js
|
||||
array = sentence.split(' ') // ❌ 裸奔,泄漏成全局变量(严格模式报错)
|
||||
const array = sentence.split(' ') // ✅ 必须 const/let 声明
|
||||
```
|
||||
- **教训**:变量**一定先声明**。
|
||||
|
||||
## I. 引号不配对
|
||||
|
||||
📍来源:`../07-objects/practice.js` 练习 A(`../01-basics/notes.md` §4)
|
||||
```js
|
||||
title: "JavaScript' // ❌ 双开单关,整个文件从这里崩
|
||||
title: "JavaScript" // ✅ 开关同一种
|
||||
```
|
||||
- **现象**:从这行起后面全报错
|
||||
- **教训**:引号**开和关必须同种**。
|
||||
|
||||
## J. 变量名定义和使用对不上
|
||||
|
||||
📍来源:`../07-objects/practice.js` 练习 C
|
||||
```js
|
||||
const cat = {...}
|
||||
Object.values(car) // ❌ car is not defined(定义 cat 用 car)
|
||||
```
|
||||
- **现象**:`X is not defined`
|
||||
- **教训**:看到 `is not defined`,检查**名字是否一致**(typo)。
|
||||
|
||||
## K. 下标从 0 开始,越界读 undefined ⭐
|
||||
|
||||
📍来源:`../07-objects/practice.js` 练习 E(`../06-arrays/notes.md` §1)
|
||||
```js
|
||||
// 3 个元素:people[0] people[1] people[2]
|
||||
people[3].name // ❌ 越界:Cannot read properties of undefined
|
||||
```
|
||||
- **教训**:下标**从 0 开始**,"第 N 个" = `[N-1]`;最后一个 = `[length-1]`。
|
||||
|
||||
## L. 对象方法用了箭头函数,this 失效 ⭐⭐
|
||||
|
||||
📍来源:`../07-objects/practice.js` 练习 D(`../07-objects/notes.md` §7)
|
||||
```js
|
||||
describe: () => this.name // ❌ 箭头函数没有自己的 this → undefined
|
||||
describe: function () { return this.name } // ✅ 用 function
|
||||
describe: function () => {...} // ❌ 语法错:function 和 => 不能同时写
|
||||
```
|
||||
- **教训**:**对象方法用 `function`,不用箭头 `=>`**(箭头没有自己的 this)。两种写法只能选一种。
|
||||
|
||||
## M. const 同名声明两次
|
||||
|
||||
📍来源:`../07-objects/practice.js` 练习 D;又见 `../10-functions.js` 练习 F
|
||||
```js
|
||||
const student = {...}
|
||||
const student = {...} // ❌ Identifier 'student' has already been declared
|
||||
```
|
||||
- **教训**:同作用域 `const` 不能重名;想留"错误对照"就**注释掉**旧的,别当活代码。
|
||||
- **重演情境(10章F)**:迭代改进时【新写了一版更好的 `const repeat`,却忘了删/注释掉旧版】
|
||||
→ 两个活的 `const repeat` → 整个文件直接报错跑不了。
|
||||
改进代码时:**旧版要么删,要么注释成 ❌ 对照,绝不能两个都当活代码**。
|
||||
|
||||
## N. 以为"代码块能算出值、会自动返回"⭐⭐(概念推错,非手滑)
|
||||
|
||||
📍来源:`../10-functions.js` 概念区第 2 节(箭头函数)
|
||||
```js
|
||||
const add = (a, b) => { a + b } // ❌ 以为块算出 a+b 就会返回 → 实际返回 undefined
|
||||
const add = (a, b) => a + b // ✅ 无 {}:右边是表达式(值),自动返回
|
||||
const add = (a, b) => { return a + b } // ✅ 有 {}:块里必须手写 return
|
||||
```
|
||||
- **现象**:函数默默返回 `undefined`,**不报错**(最难查)
|
||||
- **根源**:核心一句 —— **`=>` 右边期望一个「表达式」(能算出值的东西)**。
|
||||
- 表达式(`a+b`)=【有值】→ 直接返回;能赋值 `const x = a+b` ✅
|
||||
- 代码块(`{...}`)=【一堆动作,不产值】→ 不能赋值 `const x = {a+b}` ❌ 报错
|
||||
- 所以 `{ a+b }` 里那个 `a+b` 被算出来**却被丢弃**,函数无 return → undefined
|
||||
- **教训**:别把"代码块"当"能出值的表达式"。**块不产值**,所以进了 `{}` 就必须自己 `return`。
|
||||
|
||||
## O. 箭头函数返回对象忘加括号 ⭐
|
||||
|
||||
📍来源:`../10-functions.js` 概念区第 2 节
|
||||
```js
|
||||
const make = (name) => { name: name } // ❌ { 被当代码块,返回 undefined(不报错!)
|
||||
const make = (name) => ({ name: name }) // ✅ () 逼它当表达式 → 返回对象
|
||||
```
|
||||
- **现象**:想返回对象,却得到 `undefined`,且**不报错**
|
||||
- **根源**:对象字面量和代码块都以 `{` 开头,`=> {` 会被优先当**代码块**;
|
||||
里面的 `name:` 又被当成**标签语句**(JS 给循环起名字的老语法,`名字:`),于是静默返回 undefined
|
||||
- **教训**:箭头函数**直接返回对象**,用 `()` 把对象裹起来,逼回"表达式"语境。
|
||||
|
||||
## P. 用 `^` 当乘方(其实是按位异或)⭐(能跑不报错)
|
||||
|
||||
📍来源:`../11-higher-order-functions.js` 练习 E
|
||||
```js
|
||||
8 ** 2 // 64 ✅ 乘方用 **(或 n*n)
|
||||
8 ^ 2 // 10 ❌ ^ 是"按位异或(XOR)",不是乘方!8^2=10、10^2=8
|
||||
```
|
||||
- **现象**:算出个莫名其妙的数,**不报错**(最难发现),和平方毫无关系
|
||||
- **教训**:JS 的乘方是 `**`;`^` 是位运算(异或)。从数学/别的语境迁移来的人常栽。
|
||||
|
||||
## Q. `arr[a, b]` 当成切片(其实是逗号运算符,取单个)⭐
|
||||
|
||||
📍来源:`../11-higher-order-functions.js` 练习 F
|
||||
```js
|
||||
s[1, s.length-1] // ❌ 逗号运算符只取最后一个值 → 等于 s[s.length-1],取【单个字符】
|
||||
s.slice(1) // ✅ 切片要用 .slice(见 04 章)
|
||||
```
|
||||
- **现象**:想切一段,却只得到一个字符
|
||||
- **教训**:JS 没有 `[start, end]` 切片语法;`[]` 里的逗号是"逗号运算符"。
|
||||
逗号运算符 `a, b`:**从左到右全都算一遍,但整个表达式的值 = 最后一个**
|
||||
(`(1,2,3)` 的值是 `3`)。所以 `s[1, len-1]` = `s[len-1]`,取单个字符。
|
||||
切字符串/数组一律用 `.slice(start, end)`。日常几乎用不到逗号运算符,认得它别踩坑即可。
|
||||
|
||||
---
|
||||
|
||||
## 🔁 高频报错 → 秒查表
|
||||
|
||||
| 报错信息 | 最可能的原因 | 看哪条 |
|
||||
|---|---|---|
|
||||
| `X is not a function` | 方法名拼错 | C |
|
||||
| `X is not defined` | 变量名 typo / 没声明 | H、J |
|
||||
| `Cannot read properties of undefined` | 下标越界 / 键不存在 | K |
|
||||
| `has already been declared` | const 重名 | M |
|
||||
| 打印出 `[Function: xxx]` | 方法忘加 `()` | A |
|
||||
| 屏幕啥都没有 | 忘了 console.log / 没接住返回值 | D、E |
|
||||
| 从某行起全报错 | 引号没配对 | I |
|
||||
| 函数返回 `undefined`(不报错) | `{}` 里忘 return / 箭头返回对象没加 `()` | N、O |
|
||||
| 算出莫名其妙的数(不报错) | `^` 当乘方(实为异或)/ `[a,b]` 当切片 | P、Q |
|
||||
@ -0,0 +1,81 @@
|
||||
# 通用概念 · 分号 `;`:什么时候【必须】写
|
||||
|
||||
> JS/TS 每句结尾的 `;` 绝大多数可省(引擎会自动补),但**有几种情况不写就是坑**。
|
||||
> 本篇只记"**必须写、不写就出 bug**"的场景。相关:`../09-loops.js`(无分号风格)。
|
||||
|
||||
## 背景:ASI —— 引擎会自动补分号
|
||||
|
||||
JS 有个机制叫 **ASI(Automatic Semicolon Insertion,自动分号插入)**:
|
||||
你不写 `;`,引擎在换行处**通常**会替你补上。所以这两种写法行为完全一样:
|
||||
|
||||
```js
|
||||
const a = 1; // 写了
|
||||
const a = 1 // 没写,ASI 自动补 —— 等价
|
||||
```
|
||||
|
||||
> 本仓库 `js_notes/` 统一用**无分号**风格(Prettier 默认),保持一致即可,别混着来。
|
||||
> 但无分号的前提是:**知道下面这几个 ASI 会猜错的地方,主动兜底。**
|
||||
|
||||
## ⚠️ 坑 1:下一行以 `(` 或 `[` 开头 → 必须在【行首】补 `;`
|
||||
|
||||
ASI 看到下一行还能和上一行"连成一句",就**不补**分号,于是两行被黏成一句:
|
||||
|
||||
```js
|
||||
// 💥 出错:ASI 不补,被理解成 5[1,2,3]... 或 b(...)
|
||||
const x = 5
|
||||
[1, 2, 3].forEach((n) => console.log(n)) // TypeError: Cannot read properties...
|
||||
|
||||
const b = foo
|
||||
(function () {})() // foo(function(){})() —— 把 foo 当函数调用
|
||||
```
|
||||
|
||||
**正解:在以 `(` / `[` 开头的那行,行首加 `;`**(不是上一行结尾):
|
||||
|
||||
```js
|
||||
const x = 5
|
||||
;[1, 2, 3].forEach((n) => console.log(n)) // ✅ 行首 ; 把两句切开
|
||||
```
|
||||
|
||||
口诀:**一行别以 `(` 或 `[` 开头;非要这么写,行首先放一个 `;`。**
|
||||
(常见于:上一行是赋值/表达式,下一行想直接 `[...]`.map() 或 `(...)` 立即执行。)
|
||||
|
||||
## ⚠️ 坑 2:`return` / `throw` 等后面【不能】换行
|
||||
|
||||
这几个关键字后面一旦换行,ASI 会**强制**在关键字后补 `;`,把真正想返回的东西丢掉:
|
||||
|
||||
```js
|
||||
// 💥 等于 return; 换行后的对象被当成"死代码"丢弃 → 函数返回 undefined
|
||||
function make() {
|
||||
return
|
||||
{ name: 'Tom' }
|
||||
}
|
||||
make() // undefined,不是那个对象!
|
||||
```
|
||||
|
||||
**正解:要返回的东西和 `return` 写在同一行**(对象太长就用括号包起来接着写):
|
||||
|
||||
```js
|
||||
function make() {
|
||||
return { name: 'Tom' } // ✅ 同一行
|
||||
}
|
||||
```
|
||||
|
||||
同样受影响的:`return` / `throw` / `break` / `continue` / `yield` —— **后面别换行**。
|
||||
|
||||
## ⚠️ 坑 3(顺带):同一行写两句,中间【必须】有 `;`
|
||||
|
||||
分号真正的本职工作是"分隔语句"。想把两句挤在一行,中间**必须**加 `;`:
|
||||
|
||||
```js
|
||||
let a = 1 let b = 2 // 💥 语法错误
|
||||
let a = 1; let b = 2 // ✅ 一行两句,靠 ; 分隔
|
||||
```
|
||||
|
||||
## ✅ 一句话
|
||||
|
||||
> 平时跟着仓库的**无分号**风格写就行,ASI 帮你兜底。只死记两条保命规则:
|
||||
> 1. **行首是 `(` 或 `[`** → 这一行**行首**补 `;`;
|
||||
> 2. **`return`/`throw`/`break`/`continue` 后面不换行**(要返回的写同一行)。
|
||||
>
|
||||
> 装了 Prettier 的话,保存时会自动帮你处理坑 1 的行首 `;`,更省心。
|
||||
> TS 规则完全一样(TS 是 JS 超集),只是 TS 项目社区**更偏爱每句都写 `;`** —— 那是团队约定,不是语法强制。
|
||||
@ -0,0 +1,79 @@
|
||||
# 通用概念 · `&&` / `||`:Linux(bash) vs JavaScript
|
||||
|
||||
> 跨环境对照。shell 和 JS 来回切时看这篇。相关:`../05-conditionals/notes.md` 第 5 节。
|
||||
|
||||
## 相同点:都是"短路求值"
|
||||
|
||||
"左边决定要不要看右边"—— 这个思路两边完全一致。
|
||||
|
||||
```bash
|
||||
# bash
|
||||
命令A && 命令B # A 成功才执行 B
|
||||
命令A || 命令B # A 失败才执行 B
|
||||
```
|
||||
```js
|
||||
// JS
|
||||
a && b // a 为真才看 b
|
||||
a || b // a 为假才看 b
|
||||
```
|
||||
|
||||
## 不同点:操作数是"命令" vs "值"
|
||||
|
||||
| | Linux (bash) | JavaScript |
|
||||
|---|---|---|
|
||||
| 左右两边是 | **命令**(程序) | **值 / 表达式** |
|
||||
| "真/假"看什么 | **退出码**:`0`=成功=真,非0=失败=假 | **truthy/falsy**:值本身 |
|
||||
| 返回什么 | 最后执行命令的退出码 | **操作数原值** |
|
||||
|
||||
## ⚠️ 最大的坑:`0` 的真假正好相反
|
||||
|
||||
```bash
|
||||
# Linux:退出码 0 = 成功 = 真
|
||||
true && echo yes # 打印 yes(true 的退出码是 0)
|
||||
```
|
||||
```js
|
||||
// JS:数字 0 = falsy = 假
|
||||
0 && console.log('yes') // 不打印(0 是假)
|
||||
```
|
||||
|
||||
- Linux 的 `0`:退出码 0 = "没出错" = **真**
|
||||
- JS 的 `0`:数值零 = "空/无" = **假**
|
||||
|
||||
**同一个 `0`,两个世界真假颠倒。** 这是切换时最容易栽的地方。
|
||||
|
||||
## 相同的惯用法(思路可直接迁移)
|
||||
|
||||
```bash
|
||||
# bash:|| 兜底
|
||||
result=$(cat file) || result="default"
|
||||
# bash:&& 前置守卫
|
||||
[ -f config ] && load_config
|
||||
```
|
||||
```js
|
||||
// JS:|| 设默认值
|
||||
const result = readFile() || 'default'
|
||||
// JS:&& 守卫
|
||||
config && loadConfig()
|
||||
```
|
||||
|
||||
## ⚠️ 另一个差异:两边"能串什么"
|
||||
|
||||
bash 的 `&&`/`||` 串的是**命令**(任意命令都行,`mkdir dir && cd dir`)。
|
||||
JS 的 `&&`/`||` 是**运算符**,两边只收**表达式**(有值的东西),不能塞【语句】:
|
||||
|
||||
```js
|
||||
user && user.login() // ✅ 函数调用是表达式,行(守卫)
|
||||
cache || compute() // ✅ 兜底
|
||||
true && const x = 1 // ❌ SyntaxError:const 声明是【语句】
|
||||
true && if (a) {...} // ❌ SyntaxError:if 是【语句】
|
||||
```
|
||||
|
||||
→ 所以【不能】像 shell 那样用 `&&` 串任意语句块。要按条件跑一段带 if/for/多行的逻辑,
|
||||
就老老实实用 `if`;`&&`/`||` 只适合串"一个简短表达式"(一次函数调用、一个赋值)。
|
||||
(为什么?见 `expression-vs-statement.md`:运算符两边只接表达式。)
|
||||
|
||||
## ✅ 一句话
|
||||
|
||||
> 短路逻辑 Linux 和 JS **一致**,惯用法也像(`||`兜底、`&&`守卫)。
|
||||
> 区别:**bash 操作"命令"看退出码,JS 操作"值"看 truthy/falsy**;
|
||||
> 最大的坑:**`0` 在 bash 是真(成功),在 JS 是假**。
|
||||
@ -0,0 +1,82 @@
|
||||
# 通用概念 · 值 vs 引用(value vs reference)⭐⭐
|
||||
|
||||
> 跨主题核心概念。函数传参、对象、数组章都会反复用到。JS 最重要的底层认知之一。
|
||||
|
||||
## 一、两大阵营
|
||||
|
||||
| | 原始值(primitive) | 引用类型(reference) |
|
||||
|---|---|---|
|
||||
| 有哪些 | `number` `string` `boolean` `undefined` `null` `bigint` `symbol` | `object` `array` `function` |
|
||||
| 变量里存的 | **值本身** | 指向对象的**地址(引用)** |
|
||||
| 赋值给别人时 | **复制一份副本** | **共享同一个对象** |
|
||||
| 能原地改内部吗 | ❌ 不可变 | ✅ 可以 |
|
||||
|
||||
**一句话:原始值"复制",引用类型"共享"。**
|
||||
|
||||
## 二、决定性证据:改一个,另一个变不变?
|
||||
|
||||
### 原始值 → 复制副本,各改各的
|
||||
```js
|
||||
let a = 'hello'
|
||||
let b = a // b 拿到的是【副本】
|
||||
a = 'world' // 改 a
|
||||
console.log(b) // 'hello' ← b 没变!
|
||||
```
|
||||
|
||||
### 引用类型 → 共享同一个,牵一发动全身
|
||||
```js
|
||||
let x = [1, 2, 3]
|
||||
let y = x // x 和 y 指向【同一个】数组
|
||||
x[0] = 99 // 改 x
|
||||
console.log(y) // [99, 2, 3] ← y 跟着变了!
|
||||
```
|
||||
|
||||
> 这就是分水岭:值类型复制、互不影响;引用类型共享、一改都变。
|
||||
|
||||
## 三、字符串不可变:`word[0] = 'Y'` 会怎样
|
||||
|
||||
字符串是**原始值**,不是对象,没有"内部格子供你寻址修改"。
|
||||
|
||||
```js
|
||||
let word = 'hello'
|
||||
word[0] = 'Y' // 静默无效(严格模式下报错)
|
||||
console.log(word) // 'hello' ← 没变!
|
||||
```
|
||||
|
||||
- **能读不能写**:`word[0]` 读字符 OK,但赋值无效
|
||||
- `word[0]` 读出来的是**复制的值** `'h'`,和 word 内部已无关系 —— 不通向本体
|
||||
- 严格模式(`'use strict'`)下会报 `TypeError: Cannot assign to read only property`
|
||||
|
||||
**想"改"字符串 → 造个新的再赋值回去:**
|
||||
```js
|
||||
let word = 'hello'
|
||||
word = 'Y' + word.slice(1) // 'Y' + 'ello'
|
||||
console.log(word) // 'Yello'
|
||||
```
|
||||
(这正是 04-strings D 题"首字母大写"的原理:新字符 + 剩余,再赋值)
|
||||
|
||||
## 四、对比:数组可以原地改
|
||||
|
||||
长得像(都用 `[0]`),行为相反:
|
||||
|
||||
```js
|
||||
let arr = [1, 2, 3]
|
||||
arr[0] = 99 // ✅ 数组是引用类型 → 通向对象内部 → 改得动
|
||||
console.log(arr) // [99, 2, 3]
|
||||
|
||||
let str = 'abc'
|
||||
str[0] = 'X' // ❌ 字符串不可变 → 无效
|
||||
console.log(str) // 'abc'
|
||||
```
|
||||
|
||||
## 五、为什么这很重要(伏笔)
|
||||
|
||||
- **函数传参**:传原始值 = 传副本(函数内改不动外面);传对象 = 传引用(函数内能改到外面的对象)
|
||||
- **复制对象/数组**:`let b = a` 只是共享,不是真复制;要真复制得用别的手段(后面学)
|
||||
- 很多"我明明没改它怎么变了"的 bug,根源都是引用共享
|
||||
|
||||
---
|
||||
|
||||
## ✅ 一句话记死
|
||||
|
||||
> **原始值(含字符串)= 复制的值,不可变,`[0]` 只能读;引用类型(对象/数组)= 共享的地址,可原地改,`[0]` 通向本体。**
|
||||
@ -0,0 +1,82 @@
|
||||
// 01 · 基础
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka(Cmd+Shift+P → Quokka: Start on Current File)/ 右上角 ▷ / node 本文件
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 1. console.log() —— 打印输出
|
||||
// 调试、看结果全靠它:console.log('Hello, World!')
|
||||
// console —— JS 内置的"控制台"对象
|
||||
// .log(...) —— 它的方法,作用是打印
|
||||
// 'Hello' —— 传给它的内容
|
||||
// 可一次打印多个,用逗号隔开:console.log('a =', 1)
|
||||
//
|
||||
// 2. 注释 //
|
||||
// // 后面的内容是给人看的说明,不会被执行。删掉 // 才会"激活"那行代码。
|
||||
//
|
||||
// 3. 表达式:先算再用 ⭐
|
||||
// 引号【外】的算式会先被算成结果,再拿去用(几乎所有语言通用):
|
||||
// console.log('1 + 1 =', 1 + 1) // 1 + 1 = 2 ← 这个 1+1 会先变成 2
|
||||
// - 引号【里】 '1 + 1' → 文字,原样打印
|
||||
// - 引号【外】 1 + 1 → 运算,先算成 2
|
||||
//
|
||||
// 4. 字符串引号:单 / 双 / 反 ⭐
|
||||
// '单引号' → string
|
||||
// "双引号" → string,和单引号【完全等价】
|
||||
// `反引号` → 模板字符串,有特殊能力
|
||||
// - 单引号 = 双引号,功能没区别;社区默认用单引号(Prettier 也统一成单引号)
|
||||
// - 文字里含引号时,用另一种包起来防"打架":
|
||||
// "It's a nice day" ✅ 文字有 ' → 用 " 包
|
||||
// 'It's a nice day' ❌ 报错:中间的 ' 提前结束了字符串
|
||||
// - 反引号最有用:能用 ${变量} 直接把变量嵌进字符串,还能换行:
|
||||
// const name = '小明'; console.log(`我叫${name}`) // 我叫小明
|
||||
// 注意:${} 只在反引号里生效,单双引号里写它只是普通文字。
|
||||
//
|
||||
// 5. 怎么运行一个 .js 文件 ⭐
|
||||
// ┌────────────┬──────────────────────────────────────┬──────────────────────────────┐
|
||||
// │ 文件类型 │ 特征 │ 怎么跑 │
|
||||
// ├────────────┼──────────────────────────────────────┼──────────────────────────────┤
|
||||
// │ 自包含 │ 自己定义自己用,无 document │ ▷ / Quokka / node 文件.js ✅ │
|
||||
// │ 浏览器示例 │ 靠 index.html 串多脚本共享变量, │ 浏览器 + F12 Console ✅ │
|
||||
// │ │ 或含 document │ │
|
||||
// └────────────┴──────────────────────────────────────┴──────────────────────────────┘
|
||||
// 教程第 1 天的 main.js 报 "firstName is not defined",就是因为它依赖 variable.js,
|
||||
// 属于"浏览器示例",不能用 ▷/node 单独跑。这不是 bug,是运行方式用错了。
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. console.log() 打印
|
||||
// 2. // 注释,不执行
|
||||
// 3. 表达式先算再用(1+1→2)
|
||||
// 4. 单引号=双引号(推荐单引号);反引号能嵌 ${变量}
|
||||
// 5. 自包含文件才能用 ▷/Quokka 跑
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. console.log 打印 ──────────────────────────
|
||||
console.log('Hello, World!') // Hello, World!
|
||||
|
||||
// ── 2. 一次打印多个,用逗号隔开 ────────────────────
|
||||
console.log('1 + 1 =', 1 + 1) // 1 + 1 = 2
|
||||
|
||||
// ── 3. 表达式先算再用:引号内 vs 引号外 ⭐ ──────────
|
||||
console.log('10 * 5') // 10 * 5 ← 字符串,原样(不算)
|
||||
console.log(10 * 5) // 50 ← 运算,算了
|
||||
console.log('10 * 5 =', 10 * 5) // 10 * 5 = 50
|
||||
|
||||
// ── 4. 单引号 = 双引号 ────────────────────────────
|
||||
console.log('我是单引号') // 我是单引号
|
||||
console.log("我是双引号") // 我是双引号
|
||||
|
||||
// 文字里含撇号 → 用双引号包,避免"打架"
|
||||
console.log("It's a nice day") // It's a nice day
|
||||
|
||||
// ── 5. 反引号:用 ${} 嵌变量 ⭐ ────────────────────
|
||||
const name = '小明'
|
||||
const age = 18
|
||||
console.log('老办法:' + '我叫' + name + ',今年' + age + '岁') // 老办法:我叫小明,今年18岁
|
||||
console.log(`新办法:我叫${name},今年${age}岁`) // 新办法:我叫小明,今年18岁
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// ── 6. 👇 轮到你:自己写一行,打印 (100 - 20) / 4 的结果 ──
|
||||
console.log('结果 =', (100 - 20) / 4)
|
||||
@ -0,0 +1,198 @@
|
||||
// 02 · 变量与数据类型
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区每行后注释是【预期输出】,跑完自己对答案
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 1. 什么是变量
|
||||
// 变量就是给数据起个名字,方便反复使用和修改。
|
||||
// let age = 18 // 把 18 存进名叫 age 的盒子
|
||||
// age = 19 // 改成 19
|
||||
//
|
||||
// 2. let / const / ~~var~~ ⭐
|
||||
// ┌────────┬──────────┬──────────────────────────────────┐
|
||||
// │ 关键字 │ 能改值吗 │ 什么时候用 │
|
||||
// ├────────┼──────────┼──────────────────────────────────┤
|
||||
// │ const │ ❌ 不能 │ 默认首选,值不变的都用它 │
|
||||
// │ let │ ✅ 能改 │ 需要改变的值(计数器、累加等) │
|
||||
// │ var │ ✅ 能改 │ ⚠️ 老写法,有坑,别用 │
|
||||
// └────────┴──────────┴──────────────────────────────────┘
|
||||
// 原则:优先 const,确实要改再换 let。防手滑改错,代码也更好读。
|
||||
// const PI = 3.14 // 常量,不会变
|
||||
// let count = 0 // 会变的用 let;count = count + 1 ✅
|
||||
// // PI = 3.15 // ❌ 报错:const 不能重新赋值
|
||||
// ⚠️ var 为啥"有坑"?一个典型:变量声明前就用它,var 会【静默给 undefined】
|
||||
// 把 bug 藏起来;而 const/let 会【当场报错】(TDZ),让你一眼发现。
|
||||
// (详见 10-functions.js "提升 hoisting" 一节)
|
||||
//
|
||||
// 3. 变量命名规则 ⭐
|
||||
// 必须遵守(否则报错):
|
||||
// - 只能用字母、数字、_、$;不能以数字开头
|
||||
// - 不能用关键字(如 let、const)
|
||||
// - 区分大小写(age 和 Age 是两个变量)
|
||||
// 社区惯例(建议遵守):
|
||||
// - 用 camelCase 小驼峰:firstName、isMarried、myAge
|
||||
// - 名字要有意义:age 好过 a
|
||||
//
|
||||
// 4. 八种数据类型
|
||||
// 原始类型(Primitive)—— 7 种,最常用前 3 个:
|
||||
// ┌───────────┬──────────────────┬──────────────────────────┐
|
||||
// │ 类型 │ 例子 │ 说明 │
|
||||
// ├───────────┼──────────────────┼──────────────────────────┤
|
||||
// │ number │ 18, 3.14, -5 │ 数字(整数小数不分家) │
|
||||
// │ string │ 'hi', "abc" │ 字符串 │
|
||||
// │ boolean │ true, false │ 真/假 │
|
||||
// │ undefined │ undefined │ 声明了但没赋值 │
|
||||
// │ null │ null │ 手动表示"空" │
|
||||
// │ bigint │ 10n │ 超大整数(少见) │
|
||||
// │ symbol │ Symbol() │ 唯一标识(少见) │
|
||||
// └───────────┴──────────────────┴──────────────────────────┘
|
||||
// 引用类型(非原始):object —— {}、[]、函数等(后面专门讲)
|
||||
//
|
||||
// 5. typeof —— 查一个值是什么类型 ⭐
|
||||
// typeof 18 // 'number'
|
||||
// typeof 'hi' // 'string'
|
||||
// typeof true // 'boolean'
|
||||
// typeof undefined // 'undefined'
|
||||
// 小坑:typeof null 会返回 'object',这是 JS 历史遗留 bug,记住即可。
|
||||
//
|
||||
// ── 三个最爱混的值,typeof 都出人意料(速查)⭐⭐ ──
|
||||
// ┌───────────┬──────────────┬────────────────────────────────────┐
|
||||
// │ 值 │ typeof 结果 │ 一句话 + 坑 │
|
||||
// ├───────────┼──────────────┼────────────────────────────────────┤
|
||||
// │ undefined │ 'undefined' │ 系统给的"还没值";自成一个类型 │
|
||||
// │ null │ 'object' ⚠️ │ 你给的"空";typeof 说它 object 是坑 │
|
||||
// │ NaN │ 'number' ⚠️ │ 无效数字,属 number;NaN!==NaN 是坑 │
|
||||
// └───────────┴──────────────┴────────────────────────────────────┘
|
||||
// 记:null 不是真 object(历史 bug);NaN 不是"不是数字",它就是 number。
|
||||
// 详见下方 §6(undefined vs null)、§7(NaN)。
|
||||
//
|
||||
// 6. undefined vs null ⭐
|
||||
// 两个都表示"没有",区别在【谁放的、是不是有意】:
|
||||
// - undefined:系统给的"还没值"(默认状态)。像杯子你还没倒过。
|
||||
// - null:你主动给的"这里【有意】空着"(刻意清空/占位)。像杯子你特意倒空并标记。
|
||||
// let x; console.log(x) // undefined ← 没赋值,系统默认
|
||||
// let y = null // null ← 你主动清空
|
||||
// null === undefined // false ← 概念不同:一个"未设置",一个"设为空"
|
||||
//
|
||||
// ── null 到底是什么?怎么理解 ⭐ ──
|
||||
// null 是一个【独立的原始值】,自成一类,类里只有它一个成员;含义="有意的空"。
|
||||
// • typeof null // 'object' ← 这是 1995 年的历史 bug(底层用全 0 表示,被误判)。
|
||||
// 全世界老代码都依赖它了,改了会崩,只好将错就错。→ null 不是对象,是原始值!
|
||||
// (呼应 00-concepts/value-vs-reference.md:null 明列在"原始值"栏)
|
||||
// • 怎么判断是不是 null?直接用 === 就行,可靠(不像 NaN 连自己都不等):
|
||||
// if (x === null) { ... } // ✅ null === null 成立
|
||||
// // ❌ 别用 typeof x === 'object' 判 null:{}、[] 也是 'object',会误伤
|
||||
// • 什么时候用:"这位置将来会有个对象,但现在还没有"→ 用 null 明确占位:
|
||||
// let currentUser = null // 还没登录,主动标记"当前无用户"
|
||||
// currentUser = { name: 'Tom' } // 登录后再填
|
||||
// 比放 undefined 更能表达"我【知道】这里暂时空,不是忘了"(agent 代码常见)。
|
||||
//
|
||||
// 7. NaN / Infinity 都属于 number ⭐
|
||||
// NaN(Not a Number)名字唬人,但它自己的类型偏偏是 number ——
|
||||
// 它是 number 家族里表示"无效数字"的特殊值,不是独立的第 8 种类型。
|
||||
// typeof NaN // 'number' ← 名叫"不是数字",类型却是数字!
|
||||
// typeof Infinity // 'number'
|
||||
// ┌────────────────┬──────────┬──────────────────────────────────┐
|
||||
// │ 特殊 number 值 │ 含义 │ 怎么产生 │
|
||||
// ├────────────────┼──────────┼──────────────────────────────────┤
|
||||
// │ NaN │ 无效数字 │ 'abc' - 1、Number('hi') 转换失败 │
|
||||
// │ Infinity │ 正无穷 │ 1 / 0 │
|
||||
// │ -Infinity │ 负无穷 │ -1 / 0 │
|
||||
// └────────────────┴──────────┴──────────────────────────────────┘
|
||||
// 大坑:NaN 不等于它自己 ⭐
|
||||
// NaN === NaN // false ← 连自己都不等于!
|
||||
// 所以不能用 === NaN 判断,要用专门函数:
|
||||
// Number.isNaN(x) // 这才是判断"是不是 NaN"的正确方法
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. 变量 = 给数据起名字
|
||||
// 2. 优先 const,要改才用 let,别用 var ⭐
|
||||
// 3. 命名:小驼峰、有意义、不能数字开头
|
||||
// 4. 常用类型:number / string / boolean,外加 undefined / null
|
||||
// (null 是"有意的空",独立原始值;判断用 === null,typeof null==='object' 是坑)
|
||||
// 5. typeof 查类型(注意 typeof null === 'object' 是坑)
|
||||
// 6. NaN / Infinity 都属于 number;NaN !== NaN,判断用 Number.isNaN() ⭐
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. let 可以改,const 不能改 ⭐ ─────────────────
|
||||
let age = 18
|
||||
console.log(age) // 18
|
||||
age = 19 // let 允许重新赋值
|
||||
console.log(age) // 19
|
||||
|
||||
const PI = 3.14
|
||||
console.log(PI) // 3.14
|
||||
// PI = 3.15 // ← 取消注释会报错:Assignment to constant variable
|
||||
|
||||
// ── 2. const 首选,会变的才用 let ─────────────────
|
||||
let count = 0
|
||||
count = count + 1
|
||||
count = count + 1
|
||||
console.log('count =', count) // count = 2
|
||||
|
||||
// ── 3. 常用数据类型 ───────────────────────────────
|
||||
const myNumber = 100 // number
|
||||
const myString = 'JavaScript' // string
|
||||
const myBool = true // boolean
|
||||
console.log(myNumber, myString, myBool) // 100 JavaScript true
|
||||
|
||||
// ── 4. typeof 查类型 ⭐ ───────────────────────────
|
||||
console.log(typeof 18) // number
|
||||
console.log(typeof 'hi') // string
|
||||
typeof 'hi' // ← 这行会算,但文件里【看不到】任何输出(见第 8 组)
|
||||
console.log(typeof true) // boolean
|
||||
console.log(typeof undefined) // undefined
|
||||
console.log(typeof null) // object ← 历史遗留坑,记住即可
|
||||
|
||||
// ── 5. undefined vs null ─────────────────────────
|
||||
let x // 声明但没赋值
|
||||
console.log(x) // undefined ← 系统默认
|
||||
let y = null // 主动清空
|
||||
console.log(y) // null ← 你给的
|
||||
|
||||
// ── 6. 小驼峰命名 + 反引号拼句子(复习上一章) ──────
|
||||
const firstName = 'Asabeneh'
|
||||
const country = 'Finland'
|
||||
console.log(`${firstName} lives in ${country}`) // Asabeneh lives in Finland
|
||||
|
||||
// ── 7. 数组的 typeof 是 'object' ⭐ ───────────────
|
||||
// 数组不是独立类型,本质是特殊的 object,所以 06-arrays 才专门讲
|
||||
const arr = [1, 2, 3]
|
||||
console.log(typeof arr) // object ← 不是 'array'!
|
||||
console.log(Array.isArray(arr)) // true ← 想确认是不是数组用这个
|
||||
|
||||
// ── 8. "能算" ≠ "能看见":文件里必须 console.log ⭐ ─
|
||||
typeof 'hi' // 这行会算出 'string',但在文件里【看不到】任何东西
|
||||
console.log(typeof 'hi') // string ← 用 console.log 包起来才显示
|
||||
|
||||
// typeof 是运算符不是函数 → 后面加不加括号都行:
|
||||
console.log(typeof 'hi') // string
|
||||
console.log(typeof ('hi')) // string ← 这括号只是分组,不是函数调用
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(把 ??? 换掉,取消注释,跑一下对答案)
|
||||
|
||||
// 练习 A:声明一个 const 存你的名字,再用反引号打印 "我叫 xxx"
|
||||
// const myName = '???'
|
||||
// console.log(`我叫 ${myName}`)
|
||||
|
||||
// 练习 B:声明一个 let 叫 score = 0,让它加 10,再加 5,打印结果(应为 15)
|
||||
// let score = ???
|
||||
// score = ???
|
||||
// score = ???
|
||||
// console.log('score =', score)
|
||||
|
||||
// 练习 C:用 typeof 查一下 3.14 是什么类型,打印出来(应为 number)
|
||||
// console.log(typeof ???)
|
||||
|
||||
const myName = "Jaydon"
|
||||
console.log(`我叫 ${myName}`) // 我叫 Jaydon ← 是 ${} 不是 &{} !
|
||||
|
||||
let score = 0
|
||||
score += 10
|
||||
score += 5
|
||||
console.log(score)
|
||||
@ -0,0 +1,198 @@
|
||||
// 03 · 运算符
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区每行后注释是【预期输出】,跑完自己对答案
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 1. 算术运算符
|
||||
// ┌──────┬────────────┬──────────┬──────────────────────────┐
|
||||
// │ 符号 │ 含义 │ 例子 │ 结果 │
|
||||
// ├──────┼────────────┼──────────┼──────────────────────────┤
|
||||
// │ + │ 加 │ 2 + 3 │ 5 │
|
||||
// │ - │ 减 │ 5 - 2 │ 3 │
|
||||
// │ * │ 乘 │ 4 * 3 │ 12 │
|
||||
// │ / │ 除 │ 10 / 4 │ 2.5(JS 除法不取整!) │
|
||||
// │ % │ 取余(模) │ 10 % 3 │ 1 │
|
||||
// │ ** │ 幂(次方) │ 2 ** 3 │ 8 │
|
||||
// └──────┴────────────┴──────────┴──────────────────────────┘
|
||||
// 两个初学重点:
|
||||
// - JS 里 10 / 4 = 2.5,不像有些语言整数相除会取整。
|
||||
// - % 取余超常用:判断奇偶(n % 2 === 0 就是偶数)、循环里做周期。
|
||||
//
|
||||
// ⚠️ 负数取余的坑:判奇偶要用 === 0 不要用 === 1
|
||||
// JS 里取余结果的符号跟着"被除数"走,负数取余是负的:
|
||||
// 7 % 2 // 1 -7 % 2 // -1 ← 不是 1!符号跟着 -7
|
||||
// 所以判奇偶:
|
||||
// n % 2 === 0 // ✅ 判偶数,对正负数都成立
|
||||
// n % 2 === 1 // ❌ 判奇数,遇负数会漏:-7%2 是 -1,=== 1 得 false → 误判成偶数
|
||||
// 结论:判奇偶一律用 n % 2 === 0(判偶),再用 else 处理奇数。别用 === 1。
|
||||
//
|
||||
// 2. 赋值运算符
|
||||
// = 是"赋值"(把右边存进左边),下面是简写:
|
||||
// ┌────────┬──────────────┬────────┐
|
||||
// │ 写法 │ 等价于 │ 说明 │
|
||||
// ├────────┼──────────────┼────────┤
|
||||
// │ x += 5 │ x = x + 5 │ 加等于 │
|
||||
// │ x -= 5 │ x = x - 5 │ 减等于 │
|
||||
// │ x *= 2 │ x = x * 2 │ 乘等于 │
|
||||
// │ x /= 2 │ x = x / 2 │ 除等于 │
|
||||
// └────────┴──────────────┴────────┘
|
||||
//
|
||||
// 3. 比较运算符 → 结果是布尔值 ⭐
|
||||
// ┌───────────────┬──────────────────────────────┬──────────┬──────────┐
|
||||
// │ 符号 │ 含义 │ 例子 │ 结果 │
|
||||
// ├───────────────┼──────────────────────────────┼──────────┼──────────┤
|
||||
// │ > < >= <= │ 大小比较 │ 5 > 3 │ true │
|
||||
// │ === │ 严格等于(值和类型都要相等) │ 1 === 1 │ true │
|
||||
// │ !== │ 严格不等 │ 1 !== 2 │ true │
|
||||
// │ == │ 松散等于(会偷偷转类型) │ 1 == '1' │ true ⚠️ │
|
||||
// └───────────────┴──────────────────────────────┴──────────┴──────────┘
|
||||
// ⭐⭐ 最重要的一条规则:永远用 ===,别用 ==
|
||||
// == 会做"隐式类型转换",制造诡异结果:
|
||||
// 1 == '1' // true ← 数字 1 和字符串 '1' 被判相等?!
|
||||
// 1 === '1' // false ← === 看类型,数字 ≠ 字符串,正确
|
||||
// 0 == '' // true ← 更离谱
|
||||
// 记死这条:比较一律用 === 和 !==。这是 JS 最著名的坑之一。
|
||||
//
|
||||
// 4. 逻辑运算符
|
||||
// ┌──────┬───────────┬──────────────────┐
|
||||
// │ 符号 │ 含义 │ 口诀 │
|
||||
// ├──────┼───────────┼──────────────────┤
|
||||
// │ && │ 与(AND) │ 都为真才真 │
|
||||
// │ || │ 或(OR) │ 有一个真就真 │
|
||||
// │ ! │ 非(NOT) │ 真假反转 │
|
||||
// └──────┴───────────┴──────────────────┘
|
||||
// true && false // false true || false // true !true // false
|
||||
// 常用于条件判断:age >= 18 && hasTicket(成年且有票)。
|
||||
//
|
||||
// 5. 一个坑:+ 对字符串是"拼接" ⭐
|
||||
// 1 + 2 // 3 ← 数字相加
|
||||
// '1' + '2' // '12' ← 字符串拼接
|
||||
// '1' + 2 // '12' ← 只要有一个是字符串,就拼接!
|
||||
// 1 + '2' + 3 // '123' ← 从左到右,一旦变字符串后面全拼
|
||||
// 其他运算符(- * /)不会拼接,反而会把字符串转成数字再算:
|
||||
// '6' - 2 === 4、'6' * 2 === 12。只有 + 特殊。
|
||||
//
|
||||
// 6. 类型转换:显式 vs 隐式 ⭐⭐
|
||||
// 上面这些坑的根源,都是 JS 在偷偷"转类型"。
|
||||
// 隐式转换(JS 自作主张,坑的来源)—— 你没让它转,它自己转了:
|
||||
// '5' + 3 // '53' ← 偷偷把 3 转成字符串,拼接
|
||||
// '5' - 3 // 2 ← 偷偷把 '5' 转成数字
|
||||
// 0 == '' // true ← 偷偷都转成数字比较
|
||||
// 'abc' - 1 // NaN ← 转不成数字 → NaN(会像病毒一样传染后续计算)
|
||||
// 显式转换(你主动写,推荐 ✅)—— 意图清楚、无意外:
|
||||
// Number('42') // 42 Number('abc') // NaN ← 转不了就是 NaN
|
||||
// parseInt('42px')// 42 parseFloat('3.14') // 3.14
|
||||
// String(42) // '42'
|
||||
// Boolean('') // false Boolean('hi') // true Boolean(0) // false
|
||||
// 方向性差异(记住):
|
||||
// - 数字 → 字符串:几乎总能成功
|
||||
// - 字符串 → 数字:可能失败,失败得 NaN(处理用户输入时要留心)
|
||||
// 原则:能用显式(Number()/String())就别靠隐式。
|
||||
//
|
||||
// 7. TypeScript 能克服这些坑吗?—— 部分能 ⭐
|
||||
// TS 在写代码时(编译期)检查类型,能挡一大类,但不是全部:
|
||||
// ┌──────────────────────────────────────────────┬───────────────┐
|
||||
// │ 坑 │ TS 能挡吗 │
|
||||
// ├──────────────────────────────────────────────┼───────────────┤
|
||||
// │ '5' - 3(类型不匹配的运算) │ ✅ 编译期报错 │
|
||||
// │ 0 == ''(乱比较) │ ✅ 报错 │
|
||||
// │ '5' + 3(拼接,语法合法) │ ❌ 不管 │
|
||||
// │ 外部数据(输入/请求/JSON.parse)类型错 │ ❌ 只信标注 │
|
||||
// │ 运行时才暴露的问题 │ ❌ 类型已消失 │
|
||||
// └──────────────────────────────────────────────┴───────────────┘
|
||||
// 心智模型:TS 是"写代码时的第一道墙",挡掉大部分手滑;但 + 拼接、外部数据
|
||||
// 这些盲区,仍要靠显式转换 + 校验输入兜底。理解 JS 转换原理,是用好 TS 的前提。
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. 算术:/ 不取整、% 取余判奇偶、** 次方
|
||||
// 2. 赋值简写:+= -= *= /=
|
||||
// 3. 比较永远用 === / !==,别用 == ⭐⭐
|
||||
// 4. 逻辑:&&(都真)、||(有一真)、!(反转)
|
||||
// 5. + 遇字符串会拼接,其他运算符会转数字 ⭐
|
||||
// 6. 能用显式转换(Number()/String())就别靠隐式 ⭐
|
||||
// 7. TS 能挡"类型不匹配"的坑,但挡不住 + 拼接和外部数据 —— 显式转换的习惯不能丢
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. 算术运算符 ─────────────────────────────────
|
||||
console.log(2 + 3) // 5
|
||||
console.log(5 - 2) // 3
|
||||
console.log(4 * 3) // 12
|
||||
console.log(10 / 4) // 2.5 ← JS 除法不取整!
|
||||
console.log(10 % 3) // 1 ← 取余
|
||||
console.log(2 ** 3) // 8 ← 2 的 3 次方
|
||||
|
||||
// ── 2. % 取余判断奇偶 ⭐ ──────────────────────────
|
||||
console.log(10 % 2) // 0 ← 余 0 → 偶数
|
||||
console.log(7 % 2) // 1 ← 余 1 → 奇数
|
||||
console.log(10 % 2 === 0) // true ← 判断偶数的标准写法
|
||||
|
||||
// ── 3. 赋值简写 ───────────────────────────────────
|
||||
let x = 10
|
||||
x += 5 // 等价 x = x + 5
|
||||
console.log(x) // 15
|
||||
x *= 2 // 等价 x = x * 2
|
||||
console.log(x) // 30
|
||||
|
||||
// ── 4. 比较运算符 → 布尔值 ────────────────────────
|
||||
console.log(5 > 3) // true
|
||||
console.log(5 <= 4) // false
|
||||
console.log(1 === 1) // true
|
||||
console.log(1 !== 2) // true
|
||||
|
||||
// ── 5. === vs == 的坑 ⭐⭐(最重要)────────────────
|
||||
console.log(1 === '1') // false ← 严格:类型不同 → 不等(正确做法)
|
||||
console.log(1 == '1') // true ← 松散:偷偷转类型 → 相等(危险!)
|
||||
console.log(0 == '') // true ← == 的离谱行为
|
||||
// 结论:永远用 ===,别用 ==
|
||||
|
||||
// ── 6. 逻辑运算符 ─────────────────────────────────
|
||||
console.log(true && false) // false ← 都真才真
|
||||
console.log(true || false) // true ← 有一真就真
|
||||
console.log(!true) // false ← 反转
|
||||
|
||||
const age = 20
|
||||
const hasTicket = true
|
||||
console.log(age >= 18 && hasTicket) // true ← 成年且有票
|
||||
|
||||
// ── 7. + 遇字符串会拼接 ⭐ ────────────────────────
|
||||
console.log(1 + 2) // 3 ← 数字相加
|
||||
console.log('1' + '2') // '12' ← 字符串拼接
|
||||
console.log('1' + 2) // '12' ← 有一个字符串就拼接
|
||||
console.log(1 + '2' + 3) // '123' ← 从左到右,变字符串后全拼
|
||||
|
||||
// 其他运算符反而会把字符串转成数字:
|
||||
console.log('6' - 2) // 4 ← - 让 '6' 变数字 6
|
||||
console.log('6' * 2) // 12 ← * 同理
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(把 ??? 换掉,取消注释,跑一下对答案)
|
||||
|
||||
// 练习 A:判断 15 是不是偶数(用 % 和 ===),应打印 false
|
||||
// console.log(15 % 2 === 0)
|
||||
|
||||
// 练习 B:一个人 age = 16,判断"成年 且 有票",应打印 false
|
||||
// const myAge = ???
|
||||
// const ticket = true
|
||||
// console.log(??? >= 18 && ticket)
|
||||
|
||||
// 练习 C:猜一下 '5' + 3 和 '5' - 3 分别是什么?先猜再取消注释验证
|
||||
// console.log('5' + 3) // 你猜:___
|
||||
// console.log('5' - 3) // 你猜:___
|
||||
|
||||
// 练习 A:判断 15 是不是偶数 → 要用 === 才是"判断"
|
||||
console.log(15 % 2 === 0) // false ← 15%2 是 1,1===0 为 false
|
||||
console.log(15 % 2) // 1 ← 对比:这只是算余数,不是判断
|
||||
|
||||
// 练习 B:age=16,判断"成年(>=18)且有票"
|
||||
const myAge = 16
|
||||
const ticket = true
|
||||
console.log(myAge >= 18 && ticket) // false ← 16 不够 18,&& 整体为 false
|
||||
|
||||
// 练习 C:先猜后验(你猜的 '53' 和 2 都对!)
|
||||
console.log('5' + 3) // '53' ← + 遇字符串 → 拼接
|
||||
console.log('5' - 3) // 2 ← - 把 '5' 转成数字 → 5-3
|
||||
|
||||
@ -0,0 +1,160 @@
|
||||
// 04 · 字符串常用方法
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区带预期输出;轮到你区只给要求,自己写
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 字符串(string)是一串文字。JS 给它内置了很多"方法"(method),
|
||||
// 可以查长度、截取、替换、大小写转换等。
|
||||
//
|
||||
// 1. 长度:.length(是属性,不是方法)⭐
|
||||
// .length 后面不加括号,因为它是"属性"不是"方法"。
|
||||
// 'hello'.length // 5
|
||||
// 区分:.length 是属性(不加括号);.toUpperCase() 是方法(加括号)。
|
||||
// 判断法:方法是"动作",要加 ();属性是"数据",不加。
|
||||
//
|
||||
// 2. 大小写
|
||||
// ┌────────────────┬────────┬────────────────────────────┐
|
||||
// │ 方法 │ 作用 │ 例子 │
|
||||
// ├────────────────┼────────┼────────────────────────────┤
|
||||
// │ .toUpperCase() │ 转大写 │ 'hi'.toUpperCase() → 'HI' │
|
||||
// │ .toLowerCase() │ 转小写 │ 'HI'.toLowerCase() → 'hi' │
|
||||
// └────────────────┴────────┴────────────────────────────┘
|
||||
//
|
||||
// 3. 按位置取字符
|
||||
// const s = 'hello'
|
||||
// s[0] // 'h' ← 下标从 0 开始!
|
||||
// s[1] // 'e'
|
||||
// s[s.length - 1] // 'o' ← 最后一个字符的通用写法
|
||||
// 关键:下标从 0 开始,第 1 个字符是 s[0],最后一个是 s[length-1]。
|
||||
//
|
||||
// 4. 截取:.slice(start, end) ⭐
|
||||
// 从 start 截到 end(不含 end)。
|
||||
// const s = 'JavaScript'
|
||||
// s.slice(0, 4) // 'Java' ← 取下标 0,1,2,3(不含 4)
|
||||
// s.slice(4) // 'Script' ← 省略 end 则取到结尾
|
||||
// s.slice(-6) // 'Script' ← 负数从右边数
|
||||
// "不含 end"是初学最容易错的点:slice(0, 4) 拿的是 4 个字符,不是 5 个。
|
||||
//
|
||||
// 5. 查找
|
||||
// ┌──────────────────┬──────────────┬───────────────────────────────┐
|
||||
// │ 方法 │ 作用 │ 返回 │
|
||||
// ├──────────────────┼──────────────┼───────────────────────────────┤
|
||||
// │ .includes('x') │ 是否包含 │ true/false │
|
||||
// │ .indexOf('x') │ 首次出现下标 │ 数字;找不到返回 -1 │
|
||||
// │ .startsWith('x') │ 是否以…开头 │ true/false │
|
||||
// │ .endsWith('x') │ 是否以…结尾 │ true/false │
|
||||
// └──────────────────┴──────────────┴───────────────────────────────┘
|
||||
// 'hello'.includes('ell') // true
|
||||
// 'hello'.indexOf('l') // 2
|
||||
// 'hello'.indexOf('z') // -1 ← 找不到是 -1,不是报错
|
||||
//
|
||||
// 6. 替换与去空格
|
||||
// 'a-b-c'.replace('-', '+') // 'a+b-c' ← 只换第一个
|
||||
// 'a-b-c'.replaceAll('-', '+') // 'a+b+c' ← 全换
|
||||
// ' hi '.trim() // 'hi' ← 去掉两端空格
|
||||
//
|
||||
// 7. 拆分:.split() ⭐(字符串 → 数组)
|
||||
// 'a,b,c'.split(',') // ['a', 'b', 'c'] ← 按逗号拆成数组
|
||||
// 'hello'.split('') // ['h','e','l','l','o'] ← 拆成单个字符
|
||||
// split 是字符串通向数组的桥梁,后面数组章会经常一起用。
|
||||
//
|
||||
// ⚠️ 一个重要特性:字符串不可变(immutable)
|
||||
// 字符串方法都不会改原字符串,而是返回一个新字符串。
|
||||
// let s = 'hello'
|
||||
// s.toUpperCase() // 'HELLO'(返回新的)
|
||||
// console.log(s) // 'hello' ← 原来的没变!
|
||||
// s = s.toUpperCase() // 想保留结果,得重新赋值
|
||||
// 记住:想要变化生效,必须把返回值赋回去(或存到新变量)。
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. .length 是属性(不加括号);方法要加 ()
|
||||
// 2. 下标从 0 开始,最后一个是 [length-1]
|
||||
// 3. .slice(start, end) 截取,不含 end ⭐
|
||||
// 4. .includes / .indexOf(找不到返回 -1)
|
||||
// 5. .split() 把字符串拆成数组 ⭐
|
||||
// 6. 字符串不可变:方法返回新串,想生效要赋值回去 ⭐
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. 长度 .length(属性,不加括号)──────────────
|
||||
console.log('hello'.length) // 5
|
||||
|
||||
// ── 2. 大小写 ─────────────────────────────────────
|
||||
console.log('hi'.toUpperCase()) // HI
|
||||
console.log('HI'.toLowerCase()) // hi
|
||||
|
||||
// ── 3. 按下标取字符(从 0 开始)────────────────────
|
||||
const s = 'hello'
|
||||
console.log(s[0]) // h
|
||||
console.log(s[s.length - 1]) // o ← 最后一个字符
|
||||
|
||||
// ── 4. 截取 .slice(start, end),不含 end ⭐ ────────
|
||||
const lang = 'JavaScript'
|
||||
console.log(lang.slice(0, 4)) // Java ← 取 0,1,2,3(不含4)
|
||||
console.log(lang.slice(4)) // Script ← 到结尾
|
||||
console.log(lang.slice(-6)) // Script ← 负数从右数
|
||||
|
||||
// ── 5. 查找 ───────────────────────────────────────
|
||||
console.log('hello'.includes('ell')) // true
|
||||
console.log('hello'.indexOf('l')) // 2
|
||||
console.log('hello'.indexOf('z')) // -1 ← 找不到是 -1
|
||||
|
||||
// ── 6. 替换 / 去空格 ──────────────────────────────
|
||||
console.log('a-b-c'.replace('-', '+')) // a+b-c ← 只换第一个
|
||||
console.log('a-b-c'.replaceAll('-', '+')) // a+b+c ← 全换
|
||||
console.log(' hi '.trim()) // hi ← 去两端空格
|
||||
|
||||
// ── 7. 拆分 .split() → 数组 ⭐ ────────────────────
|
||||
console.log('a,b,c'.split(',')) // ['a', 'b', 'c']
|
||||
console.log('hello'.split('')) // ['h','e','l','l','o']
|
||||
|
||||
// ── 8. 字符串不可变 ⭐ ────────────────────────────
|
||||
let word = 'hello'
|
||||
word.toUpperCase() // 返回新串,但没接住
|
||||
console.log(word) // hello ← 原串没变!
|
||||
word = word.toUpperCase() // 赋值回去才生效
|
||||
console.log(word) // HELLO
|
||||
|
||||
// 想直接改某个字符?改不动 —— 字符串不可变
|
||||
let text = 'hello'
|
||||
text[0] = 'x' // 非严格模式:这行被静默忽略;严格模式/ESM:直接抛 TypeError
|
||||
console.log(text) // hello ← 没变!要改只能整串重新赋值(见上面第 8 组)
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(只给要求,自己写代码)
|
||||
|
||||
// 练习 A:有字符串 const city = 'Helsinki'
|
||||
// ① 打印它的长度
|
||||
// ② 打印它的大写形式
|
||||
// ③ 打印它的第一个字符
|
||||
|
||||
const city = 'Helsinki'
|
||||
console.log(city.length)
|
||||
console.log(city.toUpperCase())
|
||||
console.log(city[0])
|
||||
|
||||
|
||||
// 练习 B:有 const email = 'user@example.com'
|
||||
// ① 用 includes 判断它是否包含 '@'(应得 true)
|
||||
// ② 用 split 按 '@' 拆开,打印结果(想想会得到什么形状)
|
||||
const email = 'user@example.com'
|
||||
console.log(email.includes('@'))
|
||||
console.log(email.split('@'))
|
||||
|
||||
|
||||
// 练习 C:有 const raw = ' JavaScript '(两端有空格)
|
||||
// 先去掉两端空格,再转成小写,最后打印。
|
||||
// 提示:去空格和转小写各有一个方法,两个可以【链式】连着写(前一个的结果接着点下一个)
|
||||
const raw = ' JavaScript '
|
||||
console.log(raw.trim().toLowerCase())
|
||||
|
||||
|
||||
|
||||
// 练习 D(动脑):const name = 'javascript'
|
||||
// 只用你学过的方法,把它变成首字母大写的 'Javascript' 再打印。
|
||||
// 提示:第一个字符大写 + 剩下的部分,用 + 拼起来。
|
||||
const name = 'javascript'
|
||||
console.log(name[0].toUpperCase() + name.slice(1, name.length))
|
||||
@ -0,0 +1,204 @@
|
||||
// 05 · 条件与布尔
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区带预期输出;轮到你区只给要求,自己写
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 程序会"做决定"—— 根据条件走不同分支。这是代码从"一条直线"变成"有逻辑"的开始。
|
||||
//
|
||||
// 1. if / else if / else
|
||||
// const age = 20
|
||||
// if (age >= 18) {
|
||||
// console.log('成年')
|
||||
// } else if (age >= 13) {
|
||||
// console.log('青少年')
|
||||
// } else {
|
||||
// console.log('儿童')
|
||||
// }
|
||||
// - 条件写在 () 里,结果要是 true/false(或真假值,见第 4 节)
|
||||
// - {} 里是满足时执行的代码
|
||||
// - 从上往下,命中第一个成立的就停,后面不再判断
|
||||
//
|
||||
// 2. 三元运算符 ? : ⭐
|
||||
// if/else 的极简版,一行搞定"二选一",而且有返回值可以赋给变量:
|
||||
// const status = age >= 18 ? '成年' : '未成年'
|
||||
// // 条件 ? 真时的值 : 假时的值
|
||||
// 适合:简单的二选一赋值。
|
||||
// 别滥用:逻辑复杂就用 if,别把三元套三元,可读性差。
|
||||
//
|
||||
// 3. switch(多分支)
|
||||
// 当一个值要和很多固定选项比对时,比一长串 else if 清爽:
|
||||
// switch (day) {
|
||||
// case 1:
|
||||
// console.log('周一')
|
||||
// break // ← 别忘!否则会"穿透"继续执行下面的 case
|
||||
// case 3:
|
||||
// console.log('周三')
|
||||
// break
|
||||
// default:
|
||||
// console.log('其他')
|
||||
// }
|
||||
// 为什么每个 case 都要 break —— "穿透"(fall-through)⭐
|
||||
// 关键:case 不是"独立的盒子",而是"跳转的入口(标签)"。匹配成功后程序从那个
|
||||
// 入口跳进去,然后一路往下执行,撞到 break 才跳出。不写 break 就会"掉进"下一个 case。
|
||||
// day = 1 时若各 case 都不写 break → 输出:周一 周二 周三(全跑了)
|
||||
// 跨语言对比(纠正常见误解):
|
||||
// ┌───────────────────────────┬──────────────────────────────────────┐
|
||||
// │ 语言 │ 要 break 吗 │
|
||||
// ├───────────────────────────┼──────────────────────────────────────┤
|
||||
// │ C / C++ / Java / JS │ ✅ 都要(JS 的 switch 就是抄 C 的) │
|
||||
// │ Python │ 没有传统 switch(match 机制不同,不穿透)│
|
||||
// └───────────────────────────┴──────────────────────────────────────┘
|
||||
// 记反了很常见:C++/Java 和 JS 一样都要写 break,不是自动 break。
|
||||
// 穿透的唯一正当用法:多个 case 共用逻辑(故意省略 break):
|
||||
// case 6:
|
||||
// case 7:
|
||||
// console.log('周末') // 6 和 7 都到这
|
||||
// break
|
||||
// 💡 因为"忘写 break"太容易出 bug,现代 JS 常少用 switch,改用 if/else if
|
||||
// 或对象查表(学对象后会讲)。知道 switch 怎么回事即可,实战非首选。
|
||||
//
|
||||
// 4. 真假值 truthy / falsy ⭐⭐(之前埋的伏笔)
|
||||
// if() 里不一定非要放 true/false —— 任何值都会被 JS 自动判成"真"或"假"。
|
||||
// 假值(falsy)—— 常记这 6 个就够:
|
||||
// false 0 ''(空字符串) null undefined NaN
|
||||
// (严格说还有 0n(BigInt 的零)和 -0,都少见,先把上面 6 个记牢)
|
||||
// 真值(truthy)—— 除了上面 6 个,其他全是真:
|
||||
// 包括非零数字、非空字符串、[] 空数组、{} 空对象(注意!空数组空对象是真)
|
||||
// if ('hello') … // 会执行
|
||||
// if (0) … // 不执行
|
||||
// if ([]) … // 会执行(容易错)
|
||||
// 用途:快速判断"有没有值"(空串是假 → 走 else 分支)。
|
||||
//
|
||||
// 5. 短路:&& / || 返回的是"操作数原值",不是布尔 ⭐⭐
|
||||
// 关键认知:JS 的 &&/|| 不返回 true/false,而是返回其中一个操作数本身。
|
||||
// 靠的是短路求值:能确定结果时立刻停下,返回"决定胜负的那个值"。
|
||||
// || :返回第一个真值(找到真就停);全假返回最后一个
|
||||
// '' || '匿名' // '匿名' ← 前面假,继续,返回后面
|
||||
// 'Tom' || '匿名' // 'Tom' ← 前面就是真,立刻返回,'匿名'看都不看
|
||||
// 0 || 100 // 100
|
||||
// && :返回第一个假值(找到假就停);全真返回最后一个
|
||||
// 'Tom' && '有值' // '有值' ← 前面真,继续,返回后面
|
||||
// '' && '有值' // '' ← 前面就是假,立刻返回它
|
||||
// 1 && 2 && 3 // 3 ← 全真,返回最后一个
|
||||
// 跨语言对比(纠正误解):
|
||||
// ┌─────────────────────────────┬───────────────────────────────────┐
|
||||
// │ 语言 │ &&/|| 返回 │
|
||||
// ├─────────────────────────────┼───────────────────────────────────┤
|
||||
// │ JS / Python / Ruby / Lua │ 操作数原值(如 '' || 'x' → 'x') │
|
||||
// │ C / C++ / Java / C# │ 严格 boolean │
|
||||
// └─────────────────────────────┴───────────────────────────────────┘
|
||||
// 记反了很常见:Python 和 JS 一样返回原值;Java/C++ 才返回布尔。
|
||||
// 两个超常用惯用法 ⭐:
|
||||
// const name = userInput || '游客' // ① || 设默认值:没填(空串是假)就用 '游客'
|
||||
// user && user.login() // ② && 做"守卫":user 存在才调用 login,避免报错
|
||||
// // (React 里满地都是: 条件 && <组件/> → 条件成立才渲染)
|
||||
// 想要纯布尔?用 !!:
|
||||
// !!'Tom' // true !!'' // false !!0 // false
|
||||
// ! 取反得布尔,!! 再反回来 → 纯 true/false。
|
||||
// 💡 为什么这样设计:JS 到处用 truthy/falsy 而非死板布尔(见第 4 节),
|
||||
// 所以 &&/|| 返回原值才顺理成章 —— 这两个知识点是一套的。
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. if / else if / else:命中第一个成立的就停
|
||||
// 2. 三元 条件 ? 真值 : 假值:简单二选一,有返回值 ⭐
|
||||
// 3. switch:多分支,每个 case 别忘 break
|
||||
// 4. falsy 常记 6 个:false 0 '' null undefined NaN(另有 0n/-0,少见);其余全 truthy(空数组空对象是真!)⭐⭐
|
||||
// 5. &&/|| 返回操作数原值(非布尔):|| 取第一个真值(设默认值),&& 取第一个假值(做守卫);
|
||||
// 要纯布尔用 !!。Python 同理,Java/C++ 才返回布尔 ⭐⭐
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. if / else if / else ───────────────────────
|
||||
const age = 20
|
||||
if (age >= 18) {
|
||||
console.log('成年') // ← 命中这个,输出:成年
|
||||
} else if (age >= 13) {
|
||||
console.log('青少年')
|
||||
} else {
|
||||
console.log('儿童')
|
||||
}
|
||||
|
||||
// ── 2. 三元运算符 ? : ⭐ ──────────────────────────
|
||||
const status = age >= 18 ? '成年' : '未成年'
|
||||
console.log(status) // 成年
|
||||
|
||||
// ── 3. switch(每个 case 别忘 break)───────────────
|
||||
const day = 3
|
||||
switch (day) {
|
||||
case 1:
|
||||
console.log('周一')
|
||||
break
|
||||
case 3:
|
||||
console.log('周三') // ← 输出:周三
|
||||
break
|
||||
default:
|
||||
console.log('其他')
|
||||
}
|
||||
|
||||
// ── 4. truthy / falsy ⭐⭐ ────────────────────────
|
||||
// 假值只有 6 个:false 0 '' null undefined NaN
|
||||
if ('hello') console.log('非空字符串 → 真') // 会执行
|
||||
if (0) console.log('这行不会执行') // 0 是假
|
||||
if ([]) console.log('空数组 → 真!') // 会执行(容易错)
|
||||
if ('') {
|
||||
console.log('空串是真')
|
||||
} else {
|
||||
console.log('空串是假 → 走这') // 会执行
|
||||
}
|
||||
|
||||
// ── 5. 短路:|| 设默认值,&& 前真才看后 ⭐ ─────────
|
||||
console.log('' || '匿名') // 匿名 ← 前面假,取后面
|
||||
console.log('Tom' || '匿名') // Tom ← 前面真,直接取
|
||||
console.log(true && '继续') // 继续
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(只给要求,自己写代码)
|
||||
|
||||
// 练习 A:给一个 const score = 75
|
||||
// 用 if/else if/else 判断等级并打印:
|
||||
// >=90 打印 'A',>=60 打印 'B',否则打印 'C'
|
||||
|
||||
const score = 75
|
||||
if (score >= 90) {
|
||||
console.log('A')
|
||||
} else if (score >= 60 ) {
|
||||
console.log('B')
|
||||
} else {
|
||||
console.log('C')
|
||||
}
|
||||
|
||||
// 练习 B:把练习 A 里"是否及格(>=60)"改用【三元运算符】写,
|
||||
// 得到 '及格' 或 '不及格',打印出来
|
||||
console.log(score >= 60 ? '及格' : '不及格')
|
||||
|
||||
|
||||
// 练习 C(truthy/falsy):有 const username = ''
|
||||
// 用 if 判断:如果 username 有值就打印它,否则打印 '游客'
|
||||
// (想想空字符串是真还是假)
|
||||
const username = ''
|
||||
if (username) {
|
||||
console.log(username)
|
||||
} else {
|
||||
console.log('游客')
|
||||
}
|
||||
|
||||
|
||||
// 练习 D(短路,动脑):用 || 把练习 C 改成【一行】,
|
||||
// 效果一样:有名字用名字,没名字用 '游客'
|
||||
console.log(username || '游客')
|
||||
|
||||
|
||||
// 练习 E(挑战):写一个三元,判断 num 是奇数还是偶数,
|
||||
// const num = 7,打印 '奇数' 或 '偶数'(回忆 % 取余)
|
||||
const num = 7
|
||||
console.log(num % 2 === 1 ? '奇数' : '偶数')
|
||||
|
||||
|
||||
if (-3) {
|
||||
console.log('true')
|
||||
} else {
|
||||
console.log('false')
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
// 06 · 数组(Array)
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区带预期输出;轮到你区只给要求,自己写
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 数组 = 有序的一组值,用 [] 包起来。JS 里最常用的数据结构。
|
||||
//
|
||||
// 0. 回顾:数组是"引用类型" ⭐
|
||||
// 复习 00-concepts/value-vs-reference.md:数组是对象/引用类型,
|
||||
// - typeof [1,2,3] → 'object'(不是 'array')
|
||||
// - 判断是不是数组用 Array.isArray(arr)
|
||||
// - 可以原地修改(和字符串不可变相反)
|
||||
// - let b = a 是共享同一个数组,不是复制
|
||||
//
|
||||
// 1. 创建 & 取值
|
||||
// const fruits = ['apple', 'banana', 'cherry']
|
||||
// fruits[0] // 'apple' ← 下标从 0 开始
|
||||
// fruits[2] // 'cherry'
|
||||
// fruits[fruits.length - 1] // 'cherry' ← 最后一个
|
||||
// fruits.length // 3 ← 长度(属性,不加括号)
|
||||
//
|
||||
// 2. 增删(会改原数组)⭐
|
||||
// ┌─────────────┬──────────────────────────────┬────────┐
|
||||
// │ 方法 │ 作用 │ 位置 │
|
||||
// ├─────────────┼──────────────────────────────┼────────┤
|
||||
// │ .push(x) │ 加到末尾 │ 尾 │
|
||||
// │ .pop() │ 删最后一个(并返回它) │ 尾 │
|
||||
// │ .unshift(x) │ 加到开头 │ 头 │
|
||||
// │ .shift() │ 删第一个(并返回它) │ 头 │
|
||||
// └─────────────┴──────────────────────────────┴────────┘
|
||||
// 记忆:push/pop 管尾,shift/unshift 管头。这四个都直接改原数组。
|
||||
//
|
||||
// 3. 查找
|
||||
// ┌────────────────┬──────────────┬────────────────────┐
|
||||
// │ 方法 │ 作用 │ 返回 │
|
||||
// ├────────────────┼──────────────┼────────────────────┤
|
||||
// │ .includes(x) │ 是否包含 │ true/false │
|
||||
// │ .indexOf(x) │ 首次出现下标 │ 数字;找不到 -1 │
|
||||
// └────────────────┴──────────────┴────────────────────┘
|
||||
//
|
||||
// 4. 截取 & 拼接
|
||||
// .slice(start, end) —— 不改原数组,返回新的(不含 end):
|
||||
// const a = [1, 2, 3, 4, 5]
|
||||
// a.slice(1, 3) // [2, 3] ← 和字符串 slice 一样,不含 end
|
||||
// a.slice(2) // [3, 4, 5]
|
||||
// .concat() / 展开 —— 合并数组:
|
||||
// [1, 2].concat([3, 4]) // [1,2,3,4]
|
||||
// [...[1, 2], ...[3, 4]] // [1,2,3,4] ← 展开语法(后面章节详讲)
|
||||
//
|
||||
// 5. ⚠️ .splice() —— 强力但会改原数组
|
||||
// splice(起点, 删几个, 要插入的...) 能删能插,直接改原数组:
|
||||
// const a = [1, 2, 3, 4]
|
||||
// a.splice(1, 2) // 从下标1删2个 → a: [1, 4]
|
||||
// const b = ['a', 'd']
|
||||
// b.splice(1, 0, 'b', 'c') // 下标1删0个,插入 b,c → b: ['a','b','c','d']
|
||||
// 第一个参数是"下标",插入发生在该下标之前 ⭐
|
||||
// 插入的新元素会占据 index 这个位置,原本在此及以后的元素全部后移。
|
||||
// 缝隙心智模型(同时解释 slice):把下标看成元素之间的"缝":
|
||||
// [ first , third ]
|
||||
// ↑ ↑ ↑
|
||||
// 缝0 缝1 缝2
|
||||
// splice(index, 0, x) = 把 x 塞进"缝 index":
|
||||
// ['first','third'].splice(0, 0, 'x') // ['x','first','third'] 塞缝0=最前
|
||||
// ['first','third'].splice(1, 0, 'x') // ['first','x','third'] 塞缝1=中间 ✅
|
||||
// ['first','third'].splice(2, 0, 'x') // ['first','third','x'] 塞缝2=最后(index=length)
|
||||
// 同一模型解释 slice(1,3):从缝1切到缝3,夹住的是下标1、2 → 所以"不含 end"。
|
||||
// ⚠️ splice 的返回值是"被删元素",不是修改后的数组:
|
||||
// const q = ['first', 'third']
|
||||
// const removed = q.splice(1, 0, 'second')
|
||||
// console.log(q) // ['first','second','third'] ← 改的是原数组,要打印它
|
||||
// console.log(removed) // [] ← 返回值是被删的元素,删0个就是空数组
|
||||
// 所以插入后要 console.log(q)(原数组),别打印 q.splice(...) 的返回值。
|
||||
// ⚠️ 别把 slice(切片,不改原,返回切出的新数组)和 splice(剪接,改原,
|
||||
// 返回被删元素)搞混!差一个字母,行为完全不同。
|
||||
//
|
||||
// 6. 数组 ↔ 字符串
|
||||
// ['a', 'b', 'c'].join('-') // 'a-b-c' ← 数组转字符串
|
||||
// 'a-b-c'.split('-') // ['a','b','c'] ← 字符串转数组(复习 04)
|
||||
//
|
||||
// 7. 一个坑:== 比不了数组内容
|
||||
// 数组是引用类型,=== 比的是"是不是同一个对象",不是内容:
|
||||
// [1, 2] === [1, 2] // false! ← 两个不同的数组对象
|
||||
// 比内容要逐个比,或转成字符串比(后面有更好的办法)。
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. 数组是引用类型(可原地改;let b=a 是共享)⭐
|
||||
// 2. [0] 取值,下标从 0;.length 长度
|
||||
// 3. push/pop 管尾,shift/unshift 管头,都改原数组 ⭐
|
||||
// 4. .slice 不改原(返回切出的新数组);.splice 改原(删/插,返回被删元素,
|
||||
// 插入后要打印原数组)—— 别搞混 ⭐
|
||||
// splice 第一个参数是下标,插入在其之前;用"缝隙模型"记(缝index)
|
||||
// 5. .join() 数组→字符串,.split() 字符串→数组
|
||||
// 6. [1,2] === [1,2] 是 false(比的是对象不是内容)⭐
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 0. 数组是引用类型(复习)──────────────────────
|
||||
console.log(typeof [1, 2, 3]) // object ← 不是 'array'
|
||||
console.log(Array.isArray([1, 2, 3])) // true
|
||||
|
||||
// ── 1. 创建 & 取值 ───────────────────────────────
|
||||
const fruits = ['apple', 'banana', 'cherry']
|
||||
console.log(fruits[0]) // apple
|
||||
console.log(fruits[fruits.length - 1]) // cherry
|
||||
console.log(fruits.length) // 3
|
||||
|
||||
// ── 2. 增删:push/pop 管尾,shift/unshift 管头 ⭐ ──
|
||||
const a = [1, 2, 3]
|
||||
a.push(4)
|
||||
console.log(a) // [1, 2, 3, 4]
|
||||
a.pop()
|
||||
console.log(a) // [1, 2, 3]
|
||||
a.unshift(0)
|
||||
console.log(a) // [0, 1, 2, 3]
|
||||
a.shift()
|
||||
console.log(a) // [1, 2, 3]
|
||||
|
||||
// ── 3. 查找 ──────────────────────────────────────
|
||||
console.log([1, 2, 3].includes(2)) // true
|
||||
console.log([1, 2, 3].indexOf(3)) // 2
|
||||
console.log([1, 2, 3].indexOf(9)) // -1
|
||||
|
||||
// ── 4. slice 不改原(不含 end)────────────────────
|
||||
const b = [1, 2, 3, 4, 5]
|
||||
console.log(b.slice(1, 3)) // [2, 3]
|
||||
console.log(b) // [1, 2, 3, 4, 5] ← 原数组没变
|
||||
|
||||
// ── 5. splice 改原(删/插)⚠️ 别和 slice 混 ────────
|
||||
const c = [1, 2, 3, 4]
|
||||
c.splice(1, 2) // 从下标1删2个
|
||||
console.log(c) // [1, 4]
|
||||
|
||||
// ── 6. 数组 ↔ 字符串 ─────────────────────────────
|
||||
console.log(['a', 'b', 'c'].join('-')) // a-b-c
|
||||
console.log('a-b-c'.split('-')) // ['a', 'b', 'c']
|
||||
|
||||
// ── 7. === 比不了内容 ────────────────────────────
|
||||
console.log([1, 2] === [1, 2]) // false ← 是两个不同对象
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(只给要求,自己写代码)
|
||||
|
||||
// 练习 A:有 const nums = [10, 20, 30]
|
||||
// ① 在末尾加上 40
|
||||
// ② 在开头加上 5
|
||||
// ③ 打印最终数组和它的长度
|
||||
// (最终应是 [5, 10, 20, 30, 40],长度 5)
|
||||
|
||||
const num = [10, 20, 30]
|
||||
num.push(40)
|
||||
num.unshift(5)
|
||||
console.log(num)
|
||||
console.log(num.length)
|
||||
|
||||
|
||||
|
||||
// 练习 B:有 const colors = ['red', 'green', 'blue']
|
||||
// ① 用 includes 判断有没有 'green'
|
||||
// ② 打印 'blue' 的下标
|
||||
// ③ 用 join 把它们拼成 'red, green, blue'(用 ', ' 连接)
|
||||
const colors = ['red', 'green', 'blue']
|
||||
console.log(colors.includes('green'))
|
||||
console.log(colors.indexOf('blue'))
|
||||
console.log(colors.join(', '))
|
||||
|
||||
|
||||
|
||||
|
||||
// 练习 C:有 const letters = ['a', 'b', 'c', 'd', 'e']
|
||||
// 用 slice 取出中间三个 ['b', 'c', 'd'],并打印。
|
||||
// 打印完再打印一次 letters 本身,确认它没被改动。
|
||||
const letters = ['a', 'b', 'c', 'd', 'e']
|
||||
console.log(letters.slice(1, 4))
|
||||
console.log(letters)
|
||||
|
||||
|
||||
// 练习 D(动脑):有 const q = ['first', 'third']
|
||||
// 用 splice 在 'first' 和 'third' 之间插入 'second',
|
||||
// 让它变成 ['first', 'second', 'third'],打印。
|
||||
// 提示:splice(位置, 删几个, 插入什么) —— 这里"删几个"是 0
|
||||
const q = ['first', 'third']
|
||||
|
||||
// ❌ splice 返回的是“被删除的元素",不是修改后的数组
|
||||
// console.log(q.splice(0, 0, 'second'))
|
||||
|
||||
// ✅
|
||||
q.splice(1, 0, 'second')
|
||||
console.log(q)
|
||||
|
||||
// 练习 E(挑战):有 const sentence = 'I love java script'
|
||||
// ① 用 split 把它拆成单词数组
|
||||
// ② 用 join 把单词用 '-' 重新连起来
|
||||
// (最终得到 'I-love-java-script')
|
||||
const sentence = 'I love java script'
|
||||
|
||||
|
||||
// ❌
|
||||
// sentence.split(' ')
|
||||
// console.log(sentence)
|
||||
// sentence.join('-')
|
||||
|
||||
|
||||
// ❌ 直接写 array = ... 而没声明。这在普通模式下能跑(JS 会偷偷创建一个全局变量),但这是坏习惯,而且埋雷:
|
||||
// - 严格模式('use strict')下会直接报错:array is not defined
|
||||
// - 它会"泄漏"成全局变量,以后可能和别处撞名,制造诡异 bug
|
||||
// array = sentence.split(' ')
|
||||
|
||||
// ✅
|
||||
const array = sentence.split(' ')
|
||||
console.log(array.join('-'))
|
||||
@ -0,0 +1,249 @@
|
||||
// 07 · 对象(Object)
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区带预期输出;轮到你区只给要求,自己写
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// 对象 = 一组"键值对"(key-value),用 {} 包起来。
|
||||
// 数组是"有序列表"(按下标),对象是"带标签的数据"(按名字)。
|
||||
// 用来表示真实世界的东西:一个人、一辆车。
|
||||
//
|
||||
// 0. 对象到底是什么?——两层含义 ⭐⭐(总纲)
|
||||
// 表面:对象 = 键值对(键: 值)的集合,用 {} 装(就是下面 §1 讲的)。
|
||||
// 本质:JS 的类型分【两大阵营】—— 原始值 vs 对象:
|
||||
// • 原始值(7 个):number / string / boolean / undefined / null / bigint / symbol
|
||||
// • 对象(就 1 个大类,却装下几乎其余一切):普通 {}、【数组】、【函数】、Date、正则……
|
||||
// 它们本质【都是对象】,只是"特殊的对象"。
|
||||
// 判据:能往它身上挂属性(键:值)的,就是对象 ——
|
||||
// const arr = [1, 2]; arr.myTag = 'hi' // 数组能挂 → 数组是对象
|
||||
// const fn = () => {}; fn.count = 5 // 函数能挂 → 函数是对象
|
||||
// typeof [] // 'object' ← 数组是对象
|
||||
// typeof (()=>{}) // 'function' ← 函数特殊显示,但它也是对象
|
||||
// 这一条解释了你早前所有疑问:typeof [] 是 object、函数能当值传/能挂属性、
|
||||
// {} 叫"对象字面量"、对象是【引用类型】(共享,见 00-concepts/value-vs-reference.md)。
|
||||
// ⚠️ typeof null 也是 'object' → 那是历史 bug,null 不是对象(见 02 章 §5)。
|
||||
// 💡 TS 里对象在【值层面完全一样】,只是多一层:可用 interface/type 描述
|
||||
// "这个对象长什么样"(有哪些键、各是什么类型)—— 第 19 章讲,现在知道有这层即可。
|
||||
//
|
||||
// 1. 创建 & 结构
|
||||
// const person = {
|
||||
// name: 'Asabeneh', // 键: 值
|
||||
// age: 100,
|
||||
// isMarried: true,
|
||||
// skills: ['HTML', 'CSS', 'JS'], // 值可以是数组
|
||||
// }
|
||||
// - 每个 键: 值 之间用逗号隔开
|
||||
// - 键(key) 是名字(字符串),值(value) 可以是任何类型(数字、字符串、数组、甚至另一个对象)
|
||||
//
|
||||
// 2. 取值:两种方式 ⭐
|
||||
// person.name // 'Asabeneh' ← 方式一:点号 .(最常用)
|
||||
// person['name'] // 'Asabeneh' ← 方式二:方括号 ['键']
|
||||
// const key = 'age'
|
||||
// person[key] // 100 ← 键名存在变量里,只能用方括号
|
||||
// 记:固定键名用 .name;键名是变量时用 [变量]。
|
||||
// person.key 和 person[key] 不一样!前者找叫 "key" 的键,后者找变量 key 的值对应的键。
|
||||
//
|
||||
// 3. 增 / 改 / 删
|
||||
// 对象是引用类型(和数组一样),可以原地改:
|
||||
// const p = { name: 'Tom' }
|
||||
// p.age = 20 // 增:直接给新键赋值
|
||||
// p.name = 'Jerry'// 改:给已有键赋新值
|
||||
// delete p.age // 删:用 delete
|
||||
// delete 是运算符,专删对象属性 —— 数组别用它 ⭐
|
||||
// delete 是一元运算符(和 typeof 同类),不是方法 → 写 delete obj.key,不是 obj.delete()。
|
||||
// delete p.age // ✅ 对象属性
|
||||
// p.delete('age') // ❌ 对象没有 delete 方法
|
||||
// ⚠️ 数组虽然是特殊对象、语法上能 delete,但会留下"空洞",不该用:
|
||||
// const arr = [1, 2, 3]
|
||||
// delete arr[1] // [1, empty, 3] ← 抹空但不移位、长度还是 3!畸形
|
||||
// 数组删元素用 splice(见 06 章):arr2.splice(1, 1) // [1, 3] 真删除、前移、长度变 2 ✅
|
||||
// ┌──────────────┬──────────────────┬────────────────┐
|
||||
// │ 目标 │ delete 能用吗 │ 该用什么 │
|
||||
// ├──────────────┼──────────────────┼────────────────┤
|
||||
// │ 对象属性 │ ✅ 正解 │ delete obj.key │
|
||||
// │ 数组元素 │ ⚠️ 留空洞,别用 │ splice │
|
||||
// │ 变量/原始值 │ ❌ 无效 │ —— │
|
||||
// └──────────────┴──────────────────┴────────────────┘
|
||||
//
|
||||
// 4. 键不存在 → undefined(不报错)⭐
|
||||
// const p = { name: 'Tom' }
|
||||
// p.height // undefined ← 没这个键,返回 undefined,不报错
|
||||
// 可以用它判断"有没有某个键":
|
||||
// if (p.name) … // name 有值 → 真
|
||||
// if (p.height) … // undefined 是假 → 不执行
|
||||
//
|
||||
// 5. 常用工具:Object.keys / values / entries ⭐
|
||||
// 这些是静态方法(复习 00-concepts/call-forms.md:数据传进去):
|
||||
// const p = { name: 'Tom', age: 20 }
|
||||
// Object.keys(p) // ['name', 'age'] ← 所有键
|
||||
// Object.values(p) // ['Tom', 20] ← 所有值
|
||||
// Object.entries(p) // [['name','Tom'], ['age',20]] ← 键值对数组
|
||||
// 注意是 Object.keys(p)(大写 Object,p 传进去),不是 p.keys()。
|
||||
//
|
||||
// 6. 检查键是否存在
|
||||
// 'name' in p // true ← in 运算符
|
||||
// 'age' in p // false
|
||||
// p.hasOwnProperty('name') // true
|
||||
//
|
||||
// 7. 对象里放函数 = "方法"
|
||||
// 值如果是函数,就叫这个对象的方法:
|
||||
// const person = {
|
||||
// name: 'Tom',
|
||||
// greet: function () {
|
||||
// return 'Hi, I am ' + this.name // this 指这个对象自己
|
||||
// },
|
||||
// }
|
||||
// person.greet() // 'Hi, I am Tom'
|
||||
// this 指"调用这个方法的对象"。这里 person.greet() 里的 this 就是 person。
|
||||
// (this 是个大话题,后面深入;现在知道"this = 当前对象"即可)
|
||||
//
|
||||
// 8. 数组 + 对象 = 最常见的数据形状 ⭐
|
||||
// 真实数据几乎都是"对象数组":
|
||||
// const users = [
|
||||
// { name: 'Tom', age: 20 },
|
||||
// { name: 'Jerry', age: 18 },
|
||||
// ]
|
||||
// users[0].name // 'Tom' ← 先取第0个对象,再取它的 name
|
||||
// users[1].age // 18
|
||||
// 这就是后端返回的 JSON、后面高阶函数处理的典型数据。记住"数组套对象"的形状。
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 0. 本质:JS 分"原始值 vs 对象"两阵营;数组/函数本质都是对象(能挂属性)⭐⭐
|
||||
// 1. 对象 = {键: 值} 键值对;表示真实事物
|
||||
// 2. 取值:.键(固定名)/ [变量](键名是变量)⭐
|
||||
// 3. 增改直接赋值,删用 delete;是引用类型可原地改
|
||||
// 4. 键不存在返回 undefined(不报错)⭐
|
||||
// 5. Object.keys/values/entries(obj) 拿键/值/键值对 ⭐
|
||||
// 6. 对象里的函数 = 方法,用 this 指自己
|
||||
// 7. "对象数组"是最常见的数据形状 ⭐
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. 创建 & 结构 ───────────────────────────────
|
||||
const person = {
|
||||
name: 'Asabeneh',
|
||||
age: 100,
|
||||
isMarried: true,
|
||||
skills: ['HTML', 'CSS', 'JS'],
|
||||
}
|
||||
console.log(person) // 整个对象
|
||||
|
||||
// ── 2. 取值:点号 vs 方括号 ⭐ ────────────────────
|
||||
console.log(person.name) // Asabeneh
|
||||
console.log(person['age']) // 100
|
||||
const key = 'isMarried'
|
||||
console.log(person[key]) // true ← 键名在变量里,只能用方括号
|
||||
console.log(person.skills[0]) // HTML ← 值是数组,继续用下标
|
||||
|
||||
// ── 3. 增 / 改 / 删 ──────────────────────────────
|
||||
const p = { name: 'Tom' }
|
||||
p.age = 20 // 增
|
||||
p.name = 'Jerry' // 改
|
||||
delete p.age // 删
|
||||
console.log(p) // { name: 'Jerry' }
|
||||
|
||||
// ── 4. 键不存在 → undefined ⭐ ────────────────────
|
||||
console.log(person.height) // undefined ← 不报错
|
||||
|
||||
// ── 5. Object.keys / values / entries ⭐ ─────────
|
||||
const q = { name: 'Tom', age: 20 }
|
||||
console.log(Object.keys(q)) // ['name', 'age']
|
||||
console.log(Object.values(q)) // ['Tom', 20]
|
||||
console.log(Object.entries(q)) // [['name','Tom'], ['age',20]]
|
||||
|
||||
// ── 6. 检查键是否存在 ────────────────────────────
|
||||
console.log('name' in q) // true
|
||||
console.log('height' in q) // false
|
||||
|
||||
// ── 7. 对象里的方法 + this ───────────────────────
|
||||
const dog = {
|
||||
name: 'WangCai',
|
||||
bark: function () {
|
||||
return this.name + ' says woof'
|
||||
},
|
||||
}
|
||||
console.log(dog.bark()) // WangCai says woof
|
||||
|
||||
// ── 8. 对象数组(最常见形状)⭐ ──────────────────
|
||||
const users = [
|
||||
{ name: 'Tom', age: 20 },
|
||||
{ name: 'Jerry', age: 18 },
|
||||
]
|
||||
console.log(users[0].name) // Tom
|
||||
console.log(users[1].age) // 18
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(只给要求,自己写代码)
|
||||
|
||||
// 练习 A:创建一个对象 book,包含:
|
||||
// title: 'JavaScript', pages: 300, published: true
|
||||
// 然后分别用【点号】打印 title,用【方括号】打印 pages
|
||||
|
||||
const book = {
|
||||
title: "JavaScript",
|
||||
pages: 64,
|
||||
published: true
|
||||
|
||||
}
|
||||
console.log(book.title)
|
||||
console.log(book['pages'])
|
||||
|
||||
|
||||
// 练习 B:接着上面的 book
|
||||
// ① 给它增加一个新键 author,值是你的名字
|
||||
// ② 把 pages 改成 350
|
||||
// ③ 删除 published 这个键
|
||||
// ④ 打印最终的 book
|
||||
|
||||
book['author'] = "author"
|
||||
delete book['published']
|
||||
console.log(book)
|
||||
|
||||
|
||||
// 练习 C:有 const car = { brand: 'Tesla', color: 'red', year: 2024 }
|
||||
// ① 用 Object.keys 打印所有键
|
||||
// ② 用 Object.values 打印所有值
|
||||
// ③ 判断 car 里有没有 'price' 这个键(用 in)
|
||||
const cat = { brand: 'Tesla', color: 'red', year: 2024 }
|
||||
console.log(Object.keys(cat))
|
||||
console.log(Object.values(cat))
|
||||
console.log("price" in cat)
|
||||
|
||||
|
||||
|
||||
// 练习 D:有 const student = { name: 'Amy', grade: 90 }
|
||||
// 给它加一个方法 describe,调用时返回 'Amy: 90'
|
||||
// (提示:方法里用 this.name 和 this.grade)
|
||||
// 写完调用它并打印
|
||||
|
||||
// ❌
|
||||
// const student = { name: 'Amy', grade: 90, describe: () => {
|
||||
// return this.name + ": " + this.grade
|
||||
// }}
|
||||
|
||||
|
||||
const student = {
|
||||
name: 'Amy',
|
||||
grade: 90,
|
||||
describe: function() {
|
||||
return this.name + ": " + this.grade
|
||||
}
|
||||
}
|
||||
console.log(student.describe())
|
||||
|
||||
|
||||
// 练习 E(挑战):有下面这个对象数组
|
||||
// const people = [
|
||||
// { name: 'Tom', age: 20 },
|
||||
// { name: 'Jerry', age: 18 },
|
||||
// { name: 'Spike', age: 25 },
|
||||
// ]
|
||||
// 打印出第 2 个人(Jerry)的年龄,以及第 3 个人的名字。
|
||||
const people = [
|
||||
{ name: 'Tom', age: 20 },
|
||||
{ name: 'Jerry', age: 18 },
|
||||
{ name: 'Spike', age: 25 },
|
||||
]
|
||||
console.log(people[1].age + "\n" + people[2].name)
|
||||
@ -0,0 +1,143 @@
|
||||
// 08 · Set 与 Map
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node 本文件
|
||||
// 示范区带预期输出;轮到你区只给要求,自己写
|
||||
|
||||
// ═════ 概念总结 ═════
|
||||
//
|
||||
// Set 和 Map 是对"数组/对象"的补充。数组允许重复、对象的键只能是字符串 ——
|
||||
// 这两个新结构补上了这两个短板。
|
||||
//
|
||||
// 1. Set —— 不重复的集合 ⭐
|
||||
// Set 里的值自动去重,不会有重复项。
|
||||
// const s = new Set([1, 2, 2, 3, 3, 3])
|
||||
// console.log(s) // Set(3) {1, 2, 3} ← 重复的自动没了
|
||||
// 常用操作:
|
||||
// const s = new Set()
|
||||
// s.add(1) // 加
|
||||
// s.add(2)
|
||||
// s.add(1) // 重复的,无效
|
||||
// s.has(1) // true ← 查是否存在
|
||||
// s.delete(2) // 删
|
||||
// s.size // 1 ← 数量(注意是 size,不是 length!)
|
||||
// ⚠️ Set/Map 用 .size 数数量,不是数组的 .length。
|
||||
// 最常见用途:数组去重 ⭐⭐
|
||||
// const arr = [1, 1, 2, 3, 3]
|
||||
// const unique = [...new Set(arr)] // [1, 2, 3]
|
||||
// // ↑ 转成 Set 去重,再用 ... 展开回数组
|
||||
// 这是一个必会的惯用法:[...new Set(数组)] = 数组去重。
|
||||
//
|
||||
// 2. Map —— 更强的键值对 ⭐
|
||||
// Map 像对象,但键可以是任何类型(对象的键只能是字符串/symbol)。
|
||||
// const m = new Map()
|
||||
// m.set('name', 'Tom') // set(键, 值)
|
||||
// m.set(1, 'number key') // 键可以是数字!
|
||||
// m.set(true, 'bool key') // 甚至布尔、对象
|
||||
// m.get('name') // 'Tom' ← 用 get 取值
|
||||
// m.has(1) // true
|
||||
// m.delete(true)
|
||||
// m.size // 2
|
||||
// Map vs 对象:什么时候用哪个
|
||||
// ┌──────────┬──────────────────────────┬──────────────────────┐
|
||||
// │ │ 对象 {} │ Map │
|
||||
// ├──────────┼──────────────────────────┼──────────────────────┤
|
||||
// │ 键的类型 │ 只能字符串/symbol │ 任意类型 │
|
||||
// │ 数量 │ Object.keys(o).length │ m.size(直接) │
|
||||
// │ 顺序 │ 基本按插入序,整数键会重排│ 严格保证插入顺序 │
|
||||
// │ 遍历 │ 需 Object.keys 等 │ 直接可迭代 │
|
||||
// └──────────┴──────────────────────────┴──────────────────────┘
|
||||
// ⚠️ 对象的"顺序"有个坑:字符串键按【插入顺序】,但【整数样式的键】
|
||||
// (如 '2'、'10')会被自动提前、按升序排,无视你插入的次序:
|
||||
// Object.keys({ b: 1, 2: 1, a: 1, 10: 1 }) // ['2','10','b','a']
|
||||
// // 整数键 2,10 被提前且升序
|
||||
// Map 则严格按插入序,不管键是什么类型 —— 这才是"键是数字又在意顺序时用 Map"的真正理由。
|
||||
// 日常大多数场景用对象就够;需要"非字符串键""频繁增删""严格保序"时用 Map。
|
||||
//
|
||||
// 3. 创建时初始化
|
||||
// const s = new Set(['a', 'b', 'c']) // Set:传数组
|
||||
// const m = new Map([ // Map:传"键值对数组"(和 Object.entries 形状一样!)
|
||||
// ['name', 'Tom'],
|
||||
// ['age', 20],
|
||||
// ])
|
||||
// m.get('age') // 20
|
||||
// 呼应 07 章:Object.entries(obj) 返回的正是这种 [[k,v],...] 形状,
|
||||
// 所以 new Map(Object.entries(obj)) 能把对象转成 Map。
|
||||
//
|
||||
// 4. 遍历(先知道,循环章会详讲)
|
||||
// const s = new Set([1, 2, 3])
|
||||
// for (const x of s) console.log(x) // 1 2 3
|
||||
// const m = new Map([['a', 1], ['b', 2]])
|
||||
// for (const [k, v] of m) console.log(k, v) // a 1 / b 2
|
||||
//
|
||||
// ✅ 本主题要点回顾
|
||||
// 1. Set = 不重复集合;add/has/delete/size ⭐
|
||||
// 2. [...new Set(arr)] = 数组去重(必会惯用法)⭐⭐
|
||||
// 3. Map = 键可任意类型的键值对;set/get/has/delete/size ⭐
|
||||
// 4. Set/Map 数数量用 .size,不是 .length
|
||||
// 5. Map 保证插入顺序;需非字符串键/保序/频繁增删时用 Map,否则对象够用
|
||||
|
||||
// ═════ 示范 ═════
|
||||
|
||||
// ── 1. Set 自动去重 ⭐ ────────────────────────────
|
||||
const s = new Set([1, 2, 2, 3, 3, 3])
|
||||
console.log(s) // Set(3) {1, 2, 3}
|
||||
|
||||
// ── 2. Set 常用操作 ──────────────────────────────
|
||||
const s2 = new Set()
|
||||
s2.add(1)
|
||||
s2.add(2)
|
||||
s2.add(1) // 重复,无效
|
||||
console.log(s2.has(1)) // true
|
||||
s2.delete(2)
|
||||
console.log(s2.size) // 1 ← 注意是 size 不是 length
|
||||
|
||||
// ── 3. 数组去重惯用法 ⭐⭐ ─────────────────────────
|
||||
const arr = [1, 1, 2, 3, 3]
|
||||
console.log([...new Set(arr)]) // [1, 2, 3]
|
||||
|
||||
// ── 4. Map:键可任意类型 ⭐ ────────────────────────
|
||||
const m = new Map()
|
||||
m.set('name', 'Tom')
|
||||
m.set(1, 'number key')
|
||||
console.log(m.get('name')) // Tom
|
||||
console.log(m.get(1)) // number key
|
||||
console.log(m.has(1)) // true
|
||||
console.log(m.size) // 2
|
||||
|
||||
// ── 5. Map 初始化(键值对数组)───────────────────
|
||||
const m2 = new Map([
|
||||
['name', 'Tom'],
|
||||
['age', 20],
|
||||
])
|
||||
console.log(m2.get('age')) // 20
|
||||
|
||||
// ── 6. 遍历 ──────────────────────────────────────
|
||||
for (const x of new Set([1, 2, 3])) console.log('set item:', x) // 1,2,3
|
||||
for (const [k, v] of m2) console.log('map:', k, v) // name Tom / age 20
|
||||
|
||||
// ═════ 轮到你 ═════
|
||||
|
||||
// 👇 轮到你(只给要求,自己写代码)
|
||||
|
||||
// 练习 A:有 const nums = [5, 3, 5, 2, 3, 1, 1]
|
||||
// ① 用 Set 给它去重,得到一个新数组并打印
|
||||
// ② 打印去重后有几个元素
|
||||
|
||||
// 练习 B:创建一个空 Set,叫 tags
|
||||
// ① 依次 add 'js', 'css', 'js'(注意第二个 js 重复)
|
||||
// ② 打印 tags 的 size(想想是几)
|
||||
// ③ 判断里面有没有 'css'
|
||||
|
||||
// 练习 C:创建一个 Map,叫 scores
|
||||
// ① 存入:'Amy' → 90,'Bob' → 85
|
||||
// ② 取出并打印 Amy 的分数
|
||||
// ③ 打印 scores 里有几对(用 size)
|
||||
|
||||
// 练习 D(动脑):有一句话 const text = 'a b a c b a'
|
||||
// 数一数里面有几个【不重复】的字母(空格不算)。
|
||||
// 提示:先 split 成数组,想办法过滤掉空格,再用 Set 去重数 size。
|
||||
// (答案应该是 3:a b c)
|
||||
|
||||
// 练习 E(挑战):有 const obj = { name: 'Tom', age: 20 }
|
||||
// 把这个【对象】转成【Map】,然后打印 Map 的 size。
|
||||
// 提示:回忆 07 章的 Object.entries,它的返回形状正好能喂给 new Map()
|
||||
@ -0,0 +1,170 @@
|
||||
// 09 · 循环(Loops)
|
||||
// 跑法:Quokka 或 右上角 ▷ / node js_notes/09-loops.js
|
||||
//
|
||||
// ═════════════════════ 概念总结 ═════════════════════
|
||||
//
|
||||
// 循环 = 重复做一件事。JS 有好几种,按场景选:
|
||||
//
|
||||
// 循环种类 用途 建议
|
||||
// ─────────────────────────────────────────────────
|
||||
// for 经典计数循环,能控制下标 需要下标时
|
||||
// while 条件为真就一直循环 不知道次数、靠条件停
|
||||
// do...while 先做一次再判断 至少执行一次
|
||||
// for...of 遍历【值】(数组/字符串/Set) ⭐ 最常用,遍历元素
|
||||
// for...in 遍历【键】(对象的 key) 遍历对象属性
|
||||
//
|
||||
// ── 1. for:三段式 ⭐ ────────────────────────────
|
||||
// for (初始; 条件; 每轮结束后) { ... }
|
||||
// for (let i = 0; i < 5; i++) → i 从 0 到 4
|
||||
// i++ 是 i = i + 1 的简写
|
||||
//
|
||||
// ── 2. for...of:遍历值(最常用)⭐⭐ ─────────────
|
||||
// for (const x of [10,20,30]) → x 依次是 10,20,30
|
||||
// 直接拿到【元素本身】,不用管下标。数组/字符串/Set/Map 都能用。
|
||||
//
|
||||
// ── 3. for...in:遍历对象的键 ────────────────────
|
||||
// for (const key in obj) → key 依次是对象的每个键名
|
||||
// 注意:for...in 拿到的是【键】,要取值就用 obj[key]
|
||||
//
|
||||
// ⚠️ 口诀:of 拿值(value),in 拿键/下标(index/key)
|
||||
// ⚠️ 别用 for...in 遍历数组(会有意外的坑),数组用 for...of 或 for
|
||||
//
|
||||
// ── 4. while / do...while ───────────────────────
|
||||
// while (条件) { ... } 条件先判断,可能一次都不跑
|
||||
// do { ... } while (条件) 先跑一次再判断,至少跑一次
|
||||
// ⚠️ 循环体里必须有让条件最终变假的语句,否则【死循环】
|
||||
//
|
||||
// ── 5. break / continue ⭐ ──────────────────────
|
||||
// break 立刻【跳出】整个循环
|
||||
// continue 跳过【本轮】剩下的,直接进下一轮
|
||||
// 默认只作用于【最近的一层】循环。
|
||||
//
|
||||
// ── 6. 标签 label:给循环起名字,跳出【外层】(冷门,了解即可)──
|
||||
// 嵌套循环里,想让 break/continue 作用到外层,先给外层贴个"标签":
|
||||
// outer: for (...) { // outer: 就是标签,形如 `名字:`
|
||||
// for (...) {
|
||||
// if (...) break outer // 直接跳出【外层】,而非最近这层
|
||||
// }
|
||||
// }
|
||||
// 标签语法 `名字:` 平时几乎用不到,但要【知道它存在】——因为它和对象的
|
||||
// `键:` 长得一样,会在别处坑你(见 10 章:箭头函数 => { name: name } 里
|
||||
// name: 被当成标签,导致静默返回 undefined)。
|
||||
//
|
||||
// ═════ 要点回顾 ═════
|
||||
// 1. for 需要下标;for...of 遍历值(最常用);for...in 遍历对象键
|
||||
// 2. of 拿值,in 拿键 —— 别记反 ⭐
|
||||
// 3. 数组别用 for...in
|
||||
// 4. while 靠条件停,当心死循环
|
||||
// 5. break 跳出整个循环,continue 跳过本轮(默认作用于最近一层)⭐
|
||||
// 6. 标签 `名字:` 可让 break/continue 跳出外层;它和对象 `键:` 同形,别混(见10章坑)
|
||||
//
|
||||
// ═════════════════════ 示范 ═════════════════════
|
||||
|
||||
// ── 1. for 经典计数 ──────────────────────────────
|
||||
for (let i = 0; i < 3; i++) {
|
||||
console.log('for i =', i) // 0, 1, 2
|
||||
}
|
||||
|
||||
// 用 for + 下标遍历数组
|
||||
const arr = ['a', 'b', 'c']
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
console.log(i, arr[i]) // 0 a / 1 b / 2 c
|
||||
}
|
||||
|
||||
// ── 2. for...of 遍历值(最常用)⭐ ─────────────────
|
||||
for (const fruit of ['apple', 'banana']) {
|
||||
console.log('of:', fruit) // apple / banana ← 直接拿到值
|
||||
}
|
||||
for (const ch of 'Hi') {
|
||||
console.log('char:', ch) // H / i ← 字符串也能遍历
|
||||
}
|
||||
|
||||
// ── 3. for...in 遍历对象的键 ─────────────────────
|
||||
const person = { name: 'Tom', age: 20 }
|
||||
for (const key of Object.keys(person)) {
|
||||
console.log('key(of):', key) // name / age
|
||||
}
|
||||
for (const key in person) {
|
||||
console.log('in:', key, '=', person[key]) // name = Tom / age = 20
|
||||
}
|
||||
|
||||
// ── 4. while ─────────────────────────────────────
|
||||
let n = 3
|
||||
while (n > 0) {
|
||||
console.log('while:', n) // 3, 2, 1
|
||||
n-- // ← 必须让 n 变化,否则死循环
|
||||
}
|
||||
|
||||
// ── 5. break / continue ──────────────────────────
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i === 3) break // i 到 3 就整个跳出
|
||||
console.log('break demo:', i) // 0, 1, 2
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i % 2 === 0) continue // 偶数跳过本轮
|
||||
console.log('continue demo:', i) // 1, 3 ← 只打印奇数
|
||||
}
|
||||
|
||||
// ═════════════════════ 轮到你 ═════════════════════
|
||||
// (只给要求,自己写代码)
|
||||
|
||||
// 练习 A:用 for 循环打印 1 到 5(注意:是 1 到 5,不是 0 到 4)
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
console.log(i)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 练习 B:有 const nums = [4, 7, 2, 9]
|
||||
// 用 for...of 遍历,把每个数字打印出来
|
||||
const nums = [4, 7, 2, 9]
|
||||
for (const num of nums) {
|
||||
console.log(num)
|
||||
}
|
||||
|
||||
// 练习 C:有 const car = { brand: 'Tesla', year: 2024, color: 'red' }
|
||||
// 用 for...in 遍历,按 "键: 值" 的格式打印每一项
|
||||
// (例如 brand: Tesla)
|
||||
const car = { brand: 'Tesla', year: 2024, color: 'red' }
|
||||
for (const i in car) {
|
||||
console.log(`${i}` + ": " + car[i]) // ✅
|
||||
console.log(i + ": ", car[i]) // ❌
|
||||
console.log(i + ":", car[i]) // ✅
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 练习 D:用 for 循环算出 1 + 2 + 3 + ... + 100 的总和,打印结果
|
||||
// 提示:在循环外先定义一个 let sum = 0,每轮把 i 加进去
|
||||
// (答案应该是 5050)
|
||||
let sum = 0
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
sum += i
|
||||
}
|
||||
console.log(sum)
|
||||
|
||||
|
||||
|
||||
// 练习 E(动脑):有 const nums = [3, 8, 1, 10, 5]
|
||||
// 用循环找出里面【最大】的数并打印。
|
||||
// 提示:先假设第一个是最大(let max = nums[0]),
|
||||
// 遍历时遇到更大的就更新 max。
|
||||
const nums_ = [3, 8, 1, 10, 5]
|
||||
let max = nums_[0]
|
||||
for (const num of nums_) {
|
||||
max = num > max? num : max
|
||||
}
|
||||
console.log(max)
|
||||
|
||||
|
||||
|
||||
// 练习 F(挑战):打印 1 到 20,但遇到偶数就跳过(用 continue),
|
||||
// 只打印奇数。
|
||||
for (let i = 1; i <= 20; i+=1) {
|
||||
if (i % 2 == 0) // ❌
|
||||
if (i % 2 === 0) // ✅
|
||||
|
||||
continue // ❌
|
||||
{continue} // ✅
|
||||
console.log(i)
|
||||
}
|
||||
@ -0,0 +1,313 @@
|
||||
// 10 · 函数(Functions)
|
||||
// 学练复习一体:概念总结 → 示范 → 轮到你
|
||||
// 跑法:Quokka / 右上角 ▷ / node js_notes/10-functions.js
|
||||
// 示范区带预期输出;轮到你区只给要求,自己写
|
||||
//
|
||||
// ═════════════════════ 概念总结 ═════════════════════
|
||||
//
|
||||
// 函数 = 把一段"要重复用的逻辑"打包起来,起个名字,以后一句话就能调用。
|
||||
// 它是编程从"流水账"走向"可复用、可组合"的关键。后面的异步、高阶函数、
|
||||
// 类、agent 循环,全都建立在函数之上 —— 这章是地基中的地基。
|
||||
//
|
||||
// ── 1. 两种写法:声明 vs 表达式 ⭐ ────────────────
|
||||
// // (a) 函数声明(declaration)
|
||||
// function add(a, b) {
|
||||
// return a + b
|
||||
// }
|
||||
// // (b) 函数表达式(expression)—— 把函数当"值"存进变量
|
||||
// const add = function (a, b) {
|
||||
// return a + b
|
||||
// }
|
||||
// 区别(初学先记一条):声明会被"提升"(hoisting),定义前就能调用;
|
||||
// 表达式不会,必须先定义后调用。现代代码更常用【表达式 + 箭头函数】。
|
||||
//
|
||||
// ── 函数的"名字" vs 装它的"变量名" ⭐(易混,重点)──
|
||||
// 三种写法,分清"函数自己叫啥"和"你用哪个名字调用它":
|
||||
// (1) const xxx = () => {} // 箭头函数本体【匿名】,但赋值给 xxx 后,
|
||||
// // 引擎自动把它 .name 推断成 'xxx'
|
||||
// // → 日常说"函数叫 xxx"没问题;调用用 xxx()
|
||||
// (2) const xxx = function yyy() {} // 【具名函数表达式】:函数自己叫 yyy,xxx 是变量
|
||||
// // xxx() ✅ 用变量名调用
|
||||
// // yyy() ❌ ReferenceError!yyy 只在函数【内部】可见
|
||||
// (3) function yyy() {} // 函数声明:函数就叫 yyy,直接 yyy() 调用(且会提升)
|
||||
//
|
||||
// ⭐ 重点(2 里的坑与用途):yyy 这个名字【只在函数体内部能用】,外面看不到。
|
||||
// 那 yyy 有啥用?主要一个:让函数能在【内部调用自己(递归)】,顺便 debug 时显示名字。
|
||||
// 平时几乎不这么写,了解即可。
|
||||
// 💡 呼应练习 F:想用递归让 repeat 调用自己,靠的就是"函数有个内部能引用的名字"——
|
||||
// 函数声明 function repeat(){} 的 repeat、或具名表达式的名字,都能做到;
|
||||
// 而纯箭头 const repeat = () => {} 若要递归,只能靠外层变量名 repeat 引用自己。
|
||||
//
|
||||
// ── 什么叫"提升(hoisting)"?⭐ ──
|
||||
// 一句话说透:JS 真正执行前,会先扫一遍代码,把某些声明"吊"到所在作用域的顶端。
|
||||
// 所以你能在"字面写的位置之前"就用到它。hoisting = 吊起、提起。
|
||||
// • 函数声明 function f(){} → 【整个函数】(名字+函数体)被提到顶
|
||||
// → 定义前调用 ✅ 能用
|
||||
// • 函数表达式 const f = function(){} / 箭头 → 只有【变量名】被登记,
|
||||
// "= 赋值"留在原地不动 → 定义前调用 ❌ 报错(见下)
|
||||
//
|
||||
// ── "名字提了却不让碰",提它有啥用?⭐⭐(易困惑)──
|
||||
// 提升的用途【不是】让你提前用,而是另外两件事:
|
||||
// 用途一:一进作用域就把所有声明"登记"好 → "这个名字归本作用域"从第一行
|
||||
// 起就板上钉钉,不会前半段指外层、后半段指内层(名字归属一致)。
|
||||
// 用途二:const/let 提升后标记为"未初始化,禁止访问",这段区间叫
|
||||
// 【TDZ 暂时性死区】。你若在赋值前就用它 → 【当场报错】
|
||||
// ReferenceError: Cannot access 'x' before initialization。
|
||||
// 对照老式 var:var 也提升,但预设值是 undefined → 提前用【静默给 undefined】,
|
||||
// 把 bug 藏起来。const/let 是在这个教训上改的:保留提升(用途一),
|
||||
// 但把"提前读到 undefined"升级成"提前读就报错"(用途二)。
|
||||
// → 一句话:提升负责"提前登记名字",TDZ 负责"用早了就当场拦下"。
|
||||
//
|
||||
// 实用建议:别去利用提升在定义前调用函数(代码会难读)。养成"先定义后用"
|
||||
// 的习惯,提升对你就几乎无感 —— 而 const+箭头本来就必须先定义后用。
|
||||
//
|
||||
// ── 2. 箭头函数 => ⭐⭐(pi/TS 代码里到处是)────────
|
||||
// const add = (a, b) => a + b // 无 {}:自动 return
|
||||
// const add = (a, b) => { return a + b } // 有 {}:必须自己 return
|
||||
// const square = (n) => n * n
|
||||
// const hi = () => 'hi' // 没参数也要写空括号 ()
|
||||
//
|
||||
// 起点(记住这一句,其余都是推论):
|
||||
// ⭐⭐ => 右边【期望一个表达式】—— 即"能算出一个值的东西"(a+b、n*n、'hi' 都是)。
|
||||
// • 你给它一个表达式 → 它就是那个值 → JS 直接把这个值 return 出去(这叫"隐式返回")
|
||||
// • 所以此处 return 不但多余,而且【非法】:return 是【语句】不是表达式,
|
||||
// 放在"要表达式的位置"→ 直接语法报错:
|
||||
// const add = (a, b) => return a + b // ❌ SyntaxError(这里要表达式,return 是语句)
|
||||
//
|
||||
// 那要写多行逻辑呢?→ { } 是【逃生舱】:
|
||||
// 一旦 => 右边以 { 开头,JS 特判它为【函数体代码块】(装语句的地方),不再当表达式看。
|
||||
// 代码块只是"一堆动作",本身不产出值 → JS 不猜你要交啥 → 必须自己 return,否则返回 undefined。
|
||||
// 一句话:=> 后给"值(表达式)"就自动还;开了"{代码块}"就得自己 return。
|
||||
//
|
||||
// ⚠️ 由上面那句核心推出:对象字面量 { } 也以 { 开头 → 会撞上"代码块"这条特判!
|
||||
// 想直接返回对象,用 () 裹住,强行回到"表达式"语境:
|
||||
// const make = (name) => { name: name } // ❌ { 被当代码块 → 返回 undefined
|
||||
// const make = (name) => ({ name: name }) // ✅ () 逼它当表达式 → 返回对象
|
||||
// 为什么 ❌ 那行不【报错】只是静默 undefined?因为 { 当代码块后,里面 `name:`
|
||||
// 被当成【标签语句】(JS 给循环起名字的老语法,形如 `outer:`,见 09 章),
|
||||
// 语法完全合法 → 不报错却啥也没返回。这种"静默 undefined"最难查。
|
||||
// ⚠️ 单个参数可省括号(n => n*n),但 0 个或 ≥2 个必须带括号。
|
||||
// ⚠️ 箭头函数没有自己的 this(复习 07 章坑 L:对象方法别用箭头)。
|
||||
//
|
||||
// ── 3. 参数 vs 实参、默认参数 ────────────────────
|
||||
// function greet(name) { ... } // name 是"形参"(占位)
|
||||
// greet('Tom') // 'Tom' 是"实参"(真值)
|
||||
// function greet(name = '游客') { ... } // 默认参数:没传就用默认值
|
||||
// greet() // name = '游客'
|
||||
// greet('Tom') // name = 'Tom'
|
||||
//
|
||||
// ── 4. return:把结果"交出来" ⭐ ──────────────────
|
||||
// - return 后面的值就是"函数调用的结果",可以接住:const r = add(1,2)
|
||||
// - 函数没写 return → 返回 undefined(常见坑:算了但没交出来)
|
||||
// - return 一执行,函数【立刻结束】,后面的代码不再跑
|
||||
// ⚠️ return 后面别换行!(复习分号笔记坑2:return 换行会被强行补 ; 返回 undefined)
|
||||
//
|
||||
// ── 5. 作用域 scope:变量的"可见范围"⭐ ───────────
|
||||
// - 函数里 let/const 声明的变量,只在函数内可见(出了函数就不存在)
|
||||
// - 里面能看到外面(外层变量),外面看不到里面(封装)
|
||||
// - {} 块级作用域:let/const 只在那对花括号内有效
|
||||
// function f() { const secret = 1 }
|
||||
// console.log(secret) // ❌ ReferenceError:出了函数就看不见
|
||||
//
|
||||
// ── 6. 函数是"一等公民":能当值传来传去 ⭐⭐ ───────
|
||||
// 函数本身也是一种值,可以:存进变量、当参数传给别的函数、被返回。
|
||||
// 把"函数当参数传进去"的那个函数,就叫【高阶函数】(下一章专讲):
|
||||
// [1,2,3].map(n => n * 2) // 把箭头函数传给 map
|
||||
// 💡 这正是 agent/pi 的核心套路:注册一堆"工具函数",框架在合适时机回调它们。
|
||||
//
|
||||
// ── 为什么函数还"有属性"?因为函数就是【可调用的对象】⭐ ──
|
||||
// 函数本质是【对象】(复习 07 章 §0),只是比普通对象多一个本事:能被 () 调用。
|
||||
// 既然是对象,"能挂键值对属性"就是它天生的能力:
|
||||
// • 自带属性(不是你加的,存"关于函数自己的信息"):
|
||||
// const greet = (a, b) => a + b
|
||||
// greet.name // 'greet' ← 函数的名字(上次问"函数叫啥"就是读它)
|
||||
// greet.length // 2 ← 声明了几个参数
|
||||
// • 你也能自己挂(因为它是对象)——让函数"随身带状态":
|
||||
// function counter() { counter.callCount++ }
|
||||
// counter.callCount = 0
|
||||
// counter(); counter() // counter.callCount 变成 2
|
||||
// 一条因果链把这几天的知识串起来 ⭐:
|
||||
// 函数是一等公民 → 所以它得是个【值(对象)】→ 所以能挂属性、能当参数传
|
||||
// → 所以才有 map/filter/回调 这套玩法。根子都在"一等公民"这一句。
|
||||
// 💡 对比 Python:Python 函数【同样】是对象、是一等公民(__name__ 自带属性、
|
||||
// 也能挂自定义属性、能当参数传 map)。区别只在习惯:Python 给函数挂属性较少见,
|
||||
// 且更爱用【列表推导式 [n*2 for n in xs]】而非 map。底层道理一致,直觉可迁移。
|
||||
//
|
||||
// ═════ 要点回顾 ═════
|
||||
// 1. 两种写法:function 声明(会提升)/ const = function 表达式(不提升)
|
||||
// 2. 箭头函数 (a,b) => a+b:单表达式自动 return;没自己的 this ⭐⭐
|
||||
// 3. 默认参数 name = '游客':没传就用默认
|
||||
// 4. return 交出结果;不写 return 得 undefined;return 后别换行 ⭐
|
||||
// 5. 作用域:里能看外,外看不到里;let/const 是块级 ⭐
|
||||
// 6. 函数能当值传递 → 回调 / 高阶函数(下一章)⭐⭐
|
||||
// 根源:函数是"可调用的对象"→ 所以有属性(.name/.length)、能当参数;Python 同理
|
||||
//
|
||||
// ═════════════════════ 示范 ═════════════════════
|
||||
|
||||
// ── 1. 函数声明 ──────────────────────────────────
|
||||
function add(a, b) {
|
||||
return a + b
|
||||
}
|
||||
console.log(add(2, 3)) // 5
|
||||
|
||||
// ── 2. 函数表达式(把函数存进变量)────────────────
|
||||
const multiply = function (a, b) {
|
||||
return a * b
|
||||
}
|
||||
console.log(multiply(4, 5)) // 20
|
||||
|
||||
// ── 2.5 提升 hoisting:声明能"提前用",表达式不能 ⭐ ──
|
||||
console.log(hoisted()) // works! ← 在定义【上面】就调用,函数声明被整体提升,能用
|
||||
function hoisted() {
|
||||
return 'works!'
|
||||
}
|
||||
// 对照:const/箭头是表达式,提前用会报错(TDZ)。取消注释验证:
|
||||
// console.log(notYet()) // ❌ ReferenceError: Cannot access 'notYet' before initialization
|
||||
// const notYet = () => 'nope'
|
||||
|
||||
// ── 3. 箭头函数:三种写法,越来越短 ⭐ ─────────────
|
||||
const addArrow1 = (a, b) => {
|
||||
return a + b // 带 {} 要自己写 return
|
||||
}
|
||||
const addArrow2 = (a, b) => a + b // 单表达式:自动 return
|
||||
const square = (n) => n * n // 单参数,可省外层括号(这里保留更清晰)
|
||||
const sayHi = () => 'hi' // 无参数:空括号不能省
|
||||
console.log(addArrow1(1, 2)) // 3
|
||||
console.log(addArrow2(1, 2)) // 3
|
||||
console.log(square(5)) // 25
|
||||
console.log(sayHi()) // hi
|
||||
|
||||
// ── 4. 默认参数 ──────────────────────────────────
|
||||
const greet = (name = '游客') => `你好, ${name}`
|
||||
console.log(greet()) // 你好, 游客 ← 没传,用默认
|
||||
console.log(greet('Tom')) // 你好, Tom ← 传了,用传的
|
||||
|
||||
// ── 4.5 箭头返回对象:{ 的两种身份(呼应概念 2)⭐ ──
|
||||
const makeBad = (name) => { name: name } // { 被当"代码块",不是对象
|
||||
console.log(makeBad('Tom')) // undefined ← 坑!啥都没返回
|
||||
const makeGood = (name) => ({ name: name }) // () 强制当"值" → 真的返回对象
|
||||
console.log(makeGood('Tom')) // { name: 'Tom' }
|
||||
|
||||
// ── 5. return 交出结果;不写 return → undefined ⭐ ─
|
||||
const noReturn = (x) => {
|
||||
x * 2 // 算了,但没 return 出来
|
||||
}
|
||||
console.log(noReturn(10)) // undefined ← 没交出来!
|
||||
|
||||
// return 一执行,函数立刻结束
|
||||
const firstPositive = (a, b) => {
|
||||
if (a > 0) return a // a 是正数就直接返回,下面不再跑
|
||||
return b
|
||||
}
|
||||
console.log(firstPositive(5, 9)) // 5
|
||||
console.log(firstPositive(-1, 9)) // 9
|
||||
|
||||
// ── 6. 作用域:里能看外,外看不到里 ⭐ ─────────────
|
||||
const outer = 'I am outside'
|
||||
const showScope = () => {
|
||||
const inner = 'I am inside'
|
||||
console.log(outer) // I am outside ← 里面能看到外面
|
||||
console.log(inner) // I am inside
|
||||
}
|
||||
showScope()
|
||||
// console.log(inner) // ← 取消注释会报错:inner is not defined(外面看不到里面)
|
||||
|
||||
// ── 7. 函数当值传递(回调的雏形)⭐⭐ ──────────────
|
||||
// 把一个函数作为参数传给另一个函数
|
||||
const applyTwice = (fn, x) => fn(fn(x)) // fn 是"传进来的函数"
|
||||
const inc = (n) => n + 1
|
||||
console.log(applyTwice(inc, 5)) // 7 ← inc(inc(5)) = 7
|
||||
// 💡 pi 里注册工具、事件回调,本质就是这样"把函数传进去,框架来调用"
|
||||
|
||||
// ═════════════════════ 轮到你 ═════════════════════
|
||||
// (只给要求,自己写代码)
|
||||
|
||||
// 练习 A:写一个【函数声明】叫 double,接收一个数字,返回它的两倍。
|
||||
// 调用 double(8) 并打印(应为 16)
|
||||
function double(num) {
|
||||
return num * 2
|
||||
}
|
||||
console.log(double(8))
|
||||
|
||||
// 练习 B:把练习 A 改写成【箭头函数】版本(单表达式、自动 return),
|
||||
// 存进 const doubleArrow,调用并打印
|
||||
const doubleArrow = num => num * 2
|
||||
console.log(doubleArrow(8))
|
||||
|
||||
|
||||
// 练习 C:写一个箭头函数 greetUser(name),
|
||||
// 给 name 一个默认值 'guest',返回 `Hello, xxx`。
|
||||
// 分别在"传名字"和"不传"两种情况下调用并打印
|
||||
const greetUser = (name = 'guest') => `Hello, ${name}`
|
||||
console.log(greetUser())
|
||||
console.log(greetUser("sample name"))
|
||||
|
||||
// 练习 D:写一个函数 max2(a, b),返回两个数里较大的那个。
|
||||
// 要求用到 return 提前结束(想想练习示范 5 的 firstPositive)。
|
||||
// 用 max2(3, 9) 和 max2(10, 2) 各测一次
|
||||
function max2(a, b) {
|
||||
if ( a >= b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 练习 E(动脑):下面这段为什么打印 undefined?先想再动手改好它,
|
||||
// 让它正确返回并打印 triple(7)。(提示:看 return)
|
||||
// const triple = (n) => { n * 3 }
|
||||
// console.log(triple(...)) ← 你来补,并修好 triple
|
||||
const triple = (n) => {return n * 3 }
|
||||
console.log(triple(7))
|
||||
|
||||
|
||||
|
||||
|
||||
// 练习 F(挑战,连接下一章):写一个高阶函数 repeat(fn, times),
|
||||
// 它把传进来的函数 fn 连续调用 times 次(每次打印一句话即可)。
|
||||
// 例:repeat(() => console.log('hi'), 3) 应打印三行 hi。
|
||||
// 提示:times 次 → 用循环;fn 是"传进来的函数",直接 fn() 就能调用
|
||||
|
||||
const fn = () => {console.log("执行一次fn函数")}
|
||||
|
||||
// ❌ 最初版:把 repeat 和 fn 搞混,fn(fn, times-1) —— fn 不会递归,只执行一次
|
||||
// const repeat = (fn, times) => {
|
||||
// if (times === 0) return
|
||||
// return fn(fn, times -1)
|
||||
// }
|
||||
|
||||
// ❌ 第二版:fn() 在前、判断在后 —— 能跑,但 repeat(fn, 0) 会先打印再停 = 打印 1 次(边界错)
|
||||
// const repeat = (fn, times) => {
|
||||
// fn()
|
||||
// times -= 1
|
||||
// if (times === 0) return
|
||||
// repeat(fn, times)
|
||||
// }
|
||||
|
||||
// ✅ 终版:判断在前 → repeat(fn, 0) 直接返回,打印 0 次(边界正确)
|
||||
const repeat = (fn, times) => {
|
||||
if (times === 0) return // 先判断:times 为 0 就不打印(edge case 处理对)
|
||||
fn()
|
||||
times-=1
|
||||
// return repeat(fn, times)
|
||||
repeat(fn, times) // 不需要 return 也行(不需要返回值)
|
||||
}
|
||||
|
||||
repeat(fn, 3)
|
||||
|
||||
|
||||
// 函数也是对象:验证"赋值共享同一个"
|
||||
let x = fn
|
||||
let y = x // x、y 指向同一个函数对象(x === y 为 true)
|
||||
|
||||
// ❌ 用 .name 验证共享会"看走眼":.name 是只读属性,这次赋值【静默失败】
|
||||
y.name = "修改y的name"
|
||||
console.log(x.name) // 'fn' ← 没变!不是"没共享",是 .name 改不动(writable:false)
|
||||
|
||||
// ✅ 用普通属性才验证到共享:改 y.foo,x.foo 立刻跟着变
|
||||
y.foo = "shared!"
|
||||
console.log(x.foo) // 'shared!' ← 这才证明 x、y 是同一个对象
|
||||
|
||||
// .name 为什么改不动?看它的属性描述:writable:false = 只读
|
||||
console.log(Object.getOwnPropertyDescriptor(x, "name")) // { value:'fn', writable:false, ... }
|
||||
@ -0,0 +1,104 @@
|
||||
# 📚 我的 JavaScript 学习笔记
|
||||
|
||||
跟着 `30-Days-Of-JavaScript` 教程学,但**按知识体系整理**,方便日后复习。
|
||||
开始于 2026-07。
|
||||
|
||||
---
|
||||
|
||||
## 🧭 怎么用这个目录
|
||||
|
||||
- 每个主题就是**根目录下一个 `编号-主题.js` 单文件**(如 `08-sets-maps.js`),
|
||||
学练复习一体,内含三段:
|
||||
1. **概念总结区**(文件顶部注释,含 ASCII 表格)—— 复习先看这里
|
||||
2. **示范区** —— 带预期输出的可运行代码
|
||||
3. **轮到你区** —— 练习题,只给要求不给答案
|
||||
用 `// ═════ 概念总结 ═════` / `// ═════ 示范 ═════` / `// ═════ 轮到你 ═════` 分隔。
|
||||
- 编号(`01-`, `02-`…)只表示**推荐学习顺序**,不是"第几天"
|
||||
- 例外:`00-concepts/` 仍是文件夹(跨主题通用概念一组,多为 `.md`)
|
||||
|
||||
> 📌 结构演变:早期用 `NN-topic/`(内含 `notes.md`+`practice.js`)分开两文件,
|
||||
> 现已扁平化为**根目录单文件 `NN-topic.js`**(概念写进代码注释),减少重复、复习更快。
|
||||
|
||||
### 运行代码的三种方式
|
||||
|
||||
| 方式 | 怎么做 | 适合 |
|
||||
|---|---|---|
|
||||
| **Quokka(免费版)** | `Cmd+Shift+P` → `Quokka: Start on Current File` | 边写边看 `console.log` 的值(纯逻辑练习) |
|
||||
| **Code Runner ▷** | 右上角 ▷,或 `Ctrl+Alt+N` | 在终端跑整个文件 |
|
||||
| **浏览器 + F12** | 打开 `index.html`,看 Console | 涉及 `document`/DOM 的代码 |
|
||||
|
||||
> ⚠️ 判断能不能用 ▷/Quokka 跑:文件**自包含**(自己定义自己用、无 `document`)就能跑;
|
||||
> 依赖别的文件的变量、或含 `document` 的,要用**浏览器**。
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ 知识地图(带进度)
|
||||
|
||||
### 〇、通用概念(跨主题,随时查)
|
||||
- `00-concepts/my-mistakes.md` —— 🕳️ **我的踩坑记录**(复习先看这个!含报错秒查表)⭐
|
||||
- `00-concepts/call-forms.md` —— 三种调用形式:`值.方法()` vs `函数()` vs `类.方法()`
|
||||
- `00-concepts/value-vs-reference.md` —— 值 vs 引用:复制 vs 共享、字符串不可变 ⭐
|
||||
- `00-concepts/shortcircuit-bash-vs-js.md` —— `&&`/`||`:bash vs JS(`0` 真假相反)
|
||||
- `00-concepts/semicolons-and-asi.md` —— 分号 `;` 什么时候【必须】写(ASI 的坑:行首 `(`/`[`、`return` 换行)⭐
|
||||
- `00-concepts/expression-vs-statement.md` —— 表达式 vs 语句:谁有值/能当值用(串起箭头 `=>`、三元、return)⭐
|
||||
|
||||
### 一、基础与数据
|
||||
- [x] `01-basics` 基础:输出、注释、表达式、字符串引号、运行方式
|
||||
- [x] `02-variables` 变量与数据类型
|
||||
- [x] `03-operators` 运算符
|
||||
- [x] `04-strings` 字符串(常用方法)
|
||||
- [x] `05-conditionals` 条件与布尔
|
||||
|
||||
### 二、数据结构
|
||||
- [x] `06-arrays` 数组
|
||||
- [x] `07-objects` 对象
|
||||
- [x] `08-sets-maps` Set 与 Map
|
||||
|
||||
> 🎯 **本路线已为目标「用 pi 搭 agent + 读懂 pi 关键源码」做过适配**:
|
||||
> 异步大幅提前(agent 命脉)、新增模块与 TS 专题、前端相关暂缓。原教程的
|
||||
> day 顺序仅作参考。
|
||||
|
||||
### 三、逻辑与函数(pi 前置 · 核心)
|
||||
- [x] `09-loops` 循环
|
||||
- [x] `10-functions` 函数(声明/表达式、箭头、参数、返回、作用域、提升/TDZ)
|
||||
- [x] `11-higher-order-functions` 高阶函数 + 回调(map / filter / reduce、链式)← 读 TS 必备
|
||||
- [ ] `12-destructuring-spread` 解构与展开 ← pi 源码里到处是,读代码前置
|
||||
- [ ] `13-closures` 闭包(理解 agent 状态、回调捕获)
|
||||
|
||||
### 四、异步与工程结构(agent 的命脉)⭐
|
||||
- [ ] `14-modules` 模块 import / export ← 新增:monorepo/多包的地基
|
||||
- [ ] `15-promises-async` 异步 Promise / async-await ← 从原第18提前!每个 LLM 调用都是异步
|
||||
- [ ] `16-error-handling` 错误处理 try/catch ← 和 await 搭配
|
||||
- [ ] `17-classes` 类 class ← agent 运行时/状态管理常用
|
||||
- [ ] `18-json` JSON ← LLM 的输入输出、tool calling 都是 JSON
|
||||
|
||||
### 五、TypeScript(看懂 pi 的门票)⭐
|
||||
- [ ] `19-ts-basics` TS 基础:为什么要类型、类型标注、`interface` / `type`
|
||||
- [ ] `20-ts-generics` 泛型 `<T>`、联合类型、如何读懂函数签名
|
||||
- [ ] `21-ts-project` 工程:模块类型、`tsconfig`、怎么读 `.d.ts` 和别人的类型
|
||||
|
||||
### 六、Agent 领域 + pi 实战
|
||||
- [ ] `22-agent-concepts` 领域概念:LLM API、tool calling、agent loop 是什么
|
||||
- [ ] `23-pi-hello` 用 pi 搭一个最小可跑的 agent
|
||||
- [ ] `24-pi-source-reading` 精读 pi 源码(挑软柿子,逐行带读,不求全懂)
|
||||
|
||||
### 附:暂缓(和 pi 无关,想做前端再回来)
|
||||
- 正则表达式 / DOM 操作 / 事件监听 / Web Storage / 综合前端项目
|
||||
|
||||
---
|
||||
|
||||
## 📏 练习规矩(给协作者/AI)
|
||||
|
||||
- **练习题让用户自己改**,不要替他写答案。
|
||||
- **出题不写答案**:"轮到你"区域只给要求,不放正确答案(哪怕注释掉也不行,会剧透)。
|
||||
- **提示只给方向,不给完整写法**:如 `raw.trim().toLowerCase()` 这种整句提示等于给答案,不行;
|
||||
应改成"两个方法可以链式连写"这种点思路的说法。
|
||||
- 示范区(讲解用的例子)可带预期输出;题目区不带。
|
||||
- **用户每犯一个新错,吸收进 `00-concepts/my-mistakes.md`**(错法→现象→正解→教训),
|
||||
并带 **📍来源索引**(原题文件+练习号、相关笔记章节),方便跳回原题。复习时才记得住坑在哪。
|
||||
|
||||
## ✍️ 命名约定
|
||||
|
||||
- 文件夹:`编号-英文主题`,如 `06-arrays`
|
||||
- 笔记:`notes.md`;练习:`practice.js`
|
||||
- 一个主题内容多时,练习可拆成 `practice_01_xxx.js` 等
|
||||
@ -0,0 +1,25 @@
|
||||
// 🐿️ Quokka 试验场(免费版写法)
|
||||
// 免费版不支持 //? ,但支持实时 console.log —— 值会显示在行尾
|
||||
// 用法:Cmd+Shift+P → "Quokka: Start on Current File"
|
||||
// 然后随便改数字/代码,右边的值会实时变化,不用保存、不用切终端
|
||||
|
||||
// 1) 基本变量
|
||||
const a = 2
|
||||
const b = 3
|
||||
console.log(a + b) // 实时显示 5
|
||||
|
||||
// 2) 字符串
|
||||
const name = 'JavaScript'
|
||||
console.log(name.length) // 10
|
||||
console.log(name.toUpperCase()) // 'JAVASCRIPT'
|
||||
|
||||
// 3) 数组方法(练前 20 天最常玩的东西)
|
||||
const nums = [1, 2, 3, 4, 5]
|
||||
console.log(nums.map((n) => n * 2)) // [2, 4, 6, 8, 10]
|
||||
console.log(nums.filter((n) => n % 2 === 0)) // [2, 4]
|
||||
console.log(nums.reduce((sum, n) => sum + n, 0)) // 15
|
||||
|
||||
// 4) 试试改这里的值,看右边跟着变 👇
|
||||
let radius = 5
|
||||
const area = Math.PI * radius ** 2
|
||||
console.log(area) // 改 radius 试试,这个值会实时跟着变
|
||||
Loading…
Reference in new issue