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