🧋第 106 页:数组的操作方法复习
在前几页里我们提过 push / pop / shift / unshift, 这页开始是更灵活的“菜单修改版”:
1️⃣ slice() —— “切片”但不动原数组
let drinks = ['奶茶', '绿茶', '果汁', '咖啡'];
let newArr = drinks.slice(1, 3);
console.log(newArr); // ['绿茶', '果汁']
console.log(drinks); // ['奶茶', '绿茶', '果汁', '咖啡']
💡 它从索引 1 开始到 3(不含3)截取,不会改变原数组。
🍓 类比:
“slice”就像拿小刀切奶茶蛋糕🍰, 切出一块新蛋糕给客人,原蛋糕还完好无损。
2️⃣ splice() —— “直接修改原数组”
let drinks = ['奶茶', '绿茶', '果汁'];
drinks.splice(1, 1, '抹茶');
console.log(drinks); // ['奶茶', '抹茶', '果汁']
💬 解释:
- 第一个参数:开始位置(1)
- 第二个参数:要删几个(1)
- 后面:要插入的新元素('抹茶')
🍰 类比:
“splice”就是厨师直接在原菜单上划掉一项再写新的。 (动刀子那种✏️)
3️⃣ concat() —— 拼接数组(不改原数组)
let a = ['奶茶'];
let b = ['咖啡'];
let menu = a.concat(b);
console.log(menu); // ['奶茶','咖啡']
🍵 类比:
把“奶茶店菜单”和“咖啡馆菜单”合并成一个新菜单。
🌸第 107 页:数组的查找方法
1️⃣ indexOf() / lastIndexOf()
let drinks = ['奶茶','果汁','奶茶'];
console.log(drinks.indexOf('奶茶')); // 0
console.log(drinks.lastIndexOf('奶茶')); // 2
💡 indexOf 从左找,lastIndexOf 从右找。
🍹 类比:
“第一个奶茶在哪行?最后一个奶茶又在哪?”
2️⃣ includes()
let drinks = ['奶茶','绿茶','果汁'];
console.log(drinks.includes('绿茶')); // true
🍰 类比:
检查菜单上有没有“绿茶”这一项。
3️⃣ find() / findIndex()
let drinks = [
{ name: '奶茶', price: 10 },
{ name: '抹茶', price: 15 }
];
let item = drinks.find(d => d.price > 10);
console.log(item); // { name: '抹茶', price: 15 }
💡 find() 找到“第一个符合条件的项”; findIndex() 找到“它的位置编号”。
🍬 类比:
就像顾客说“我想找最贵的奶茶”,店员帮你定位菜单上那一项。
☀️第 108 页:排序方法(翻菜单、调顺序)
1️⃣ reverse() —— 反转数组顺序
let drinks = ['奶茶','果汁','绿茶'];
drinks.reverse();
console.log(drinks); // ['绿茶','果汁','奶茶']
🍓 类比:
原来菜单是“按冷热排列”,现在你倒着看,冷饮排前面。
2️⃣ sort() —— 排序(默认按字母顺序)
let nums = [10, 2, 30];
nums.sort();
console.log(nums); // [10, 2, 30] (错误排序!)
⚠️ 原因:sort() 会把数字当“字符串”排,比如 “10” < “2”。
👉 解决办法:
nums.sort((a, b) => a - b);
console.log(nums); // [2, 10, 30]
🍵 类比:
不说明排序规则时,店员是按“名字首字母”排菜单; 指定规则
(a-b)才是按价格从低到高排!
🌷第 109 页:数组的转换方法
1️⃣ join()
let arr = ['奶茶', '果汁', '咖啡'];
console.log(arr.join('、')); // 奶茶、果汁、咖啡
💬 把数组元素拼成字符串。 🍰 类比:
“菜单打印机”把三行项目连成一句话。
2️⃣ toString()
let arr = ['珍珠', '波霸'];
console.log(arr.toString()); // 珍珠,波霸
💡 和 join(',') 类似,但不能换分隔符。
3️⃣ flat()
let arr = [1, [2, [3]]];
console.log(arr.flat(2)); // [1,2,3]
🍹 类比:
“多层菜单合并”——把主菜单、子菜单全部摊开平铺。
☕第 110 页:数组的遍历与判断方法
1️⃣ some() 和 every()
let prices = [5, 15, 25];
console.log(prices.some(p => p > 20)); // true
console.log(prices.every(p => p > 10)); // false
💡
some():只要有一个符合条件就返回trueevery():所有都要符合才true
🍰 类比:
some= “有没有一杯加珍珠?”every= “是不是每杯都加珍珠?”
2️⃣ forEach()
let drinks = ['奶茶','抹茶','咖啡'];
drinks.forEach(d => console.log(d));
💡 遍历执行,不返回新数组。
🍵 类比:
店员一个个报单确认:“奶茶好~ 抹茶好~ 咖啡好~”
3️⃣ map()
let prices = [10, 20];
let newPrices = prices.map(p => p + 5);
console.log(newPrices); // [15,25]
🍹 类比:
所有饮品都涨价 5 元 → 生成新菜单(不改旧的)。
🎀 小可爱总结(第106~110页)
| 分类 | 方法 | 奶茶店比喻 |
|---|---|---|
| 截取 | slice | 切蛋糕不伤原体 |
| 修改 | splice | 原菜单上划掉重写 |
| 查找 | indexOf / find | 找哪杯奶茶在哪行 |
| 反转 | reverse | 倒着翻菜单 |
| 排序 | sort | 重新排价格顺序 |
| 转换 | join / flat | 菜单打印机 / 平铺 |
| 遍历 | forEach | 店员逐杯报单 |
| 条件判断 | some / every | 是否全加珍珠 |
🌈 超可爱口诀:
“slice 切蛋糕,splice 改菜单, reverse 翻页看,sort 价格排, join 打印单,flat 展平来, some 看一杯,every 全加奶~🧋”
要不要我继续讲 第111~115页? 那部分是异步执行(Event Loop、Promise、微任务宏任务) , 我会用“奶茶店点单 → 制作 → 取餐 → 清理桌面”的故事,让你彻底明白为什么:
console.log('A');
setTimeout(()=>console.log('B'));
Promise.resolve().then(()=>console.log('C'));
console.log('D');
结果是 A D C B 😆
🧋第 111 页:从 filter 和 map 过渡到事件循环
这页前半部分讲 filter() 和 map(),咱先快速复习一下~
1️⃣ filter() —— 筛选出符合条件的项
let arr = [1, 2, 3, 4, 5];
let result = arr.filter(n => n > 3);
console.log(result); // [4, 5]
🍓 类比:
filter 就像筛珍珠, 把“大于3”的珍珠留下,小珍珠全滤掉~
2️⃣ map() —— 把每一项都“加工”成新样子
let arr = [1, 2, 3];
let result = arr.map(n => n * 2);
console.log(result); // [2, 4, 6]
🍵 类比:
map 就像奶茶师傅给每杯饮品加珍珠, 原来
[1,2,3]三杯,现在都变成“加料版”[2,4,6]。 (不会动原数组!)
☀️第 112 页:事件循环(Event Loop)是什么?
这就是本章的主角登场啦!✨
💡 定义(第 112 页)
事件循环(Event Loop)是 JavaScript 用来管理任务执行顺序的机制。 因为 JS 是单线程语言,一次只能做一件事—— 它通过“循环调度任务队列”的方式,让代码看起来能“异步执行”。
🍰 类比:奶茶店的“点单系统”!
想象一家只有一个店员的奶茶店: 他既要接单、做奶茶、收钱,还要打扫桌子~ 所以他必须排队执行每件事,不然就乱套了。
⚙️ 执行机制流程图(第 112 页)
(图片上那张框图说明的逻辑是 👇)
1️⃣ 主线程(Main Thread) → 就是店员,负责处理所有同步代码。
2️⃣ 任务队列(Event Queue) → 顾客的订单排成一队等待处理。
3️⃣ 事件循环(Event Loop) → 就是“调度员”,他会一直问:
“店员忙完了吗?可以接下一个任务了吗?”
当主线程空了,就取出任务队列中的第一个任务继续执行。 这个不断取任务的过程就是——事件循环。
🌸第 113 页:宏任务(Macro Task)与微任务(Micro Task)
事件循环中的任务分为两种类型:
| 类型 | 举例 | 执行时机 |
|---|---|---|
| 宏任务(Macro Task) | setTimeout, setInterval, script整体 | 一轮事件循环执行完后 |
| 微任务(Micro Task) | Promise.then, queueMicrotask | 宏任务执行后、下一轮前 |
🍵 类比理解:
店员 = 主线程 顾客点单 = 任务 宏任务 = “整杯奶茶订单” 微任务 = “小动作加料(比如加珍珠)”
执行顺序: 1️⃣ 店员先做当前那杯奶茶(主线程) 2️⃣ 然后检查有没有“加料微任务” 3️⃣ 再接下一个顾客的奶茶单(下一个宏任务)
📘 示例:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
执行顺序是:
A → D → C → B
因为: 1️⃣ A 和 D 是主线程同步任务。 2️⃣ Promise.then 是微任务,会在本轮结束后执行。 3️⃣ setTimeout 是宏任务,要等下一轮事件循环。
🍰 类比:
你下单时(A、D)先立刻喊出来; 奶茶师加珍珠(C)马上做完; 下一个顾客的单(B)等前面全处理完再来。
☀️第 114 页:宏任务 vs 微任务 流程图
图片上那张流程图就是 👇 的意思:
┌────────────────┐
│ 执行主线程代码 │ ← A D
└─────┬──────────┘
│ 执行完后
▼
┌────────────────┐
│ 执行所有微任务 │ ← C
└─────┬──────────┘
│ 全部完成后
▼
┌────────────────┐
│ 取下一个宏任务 │ ← B
└────────────────┘
也就是:
一轮循环顺序:主线程 → 微任务 → 下一个宏任务。
🧋第 115 页:更细节的执行顺序
这页主要帮你确认细节:
✅ 常见宏任务:
- 整个
script文件 setTimeoutsetIntervalI/OUI 渲染
✅ 常见微任务:
Promise.thenMutationObserverqueueMicrotask
🍵 类比总结:
一次循环(做奶茶)流程是: 1️⃣ 店员处理当前所有同步工作(做奶茶) 2️⃣ 顺便加完所有“加料微任务”(加珍珠) 3️⃣ 最后才去接下一位顾客的订单(宏任务)
🎀 小可爱总结(第111~115页)
| 概念 | 含义 | 奶茶店类比 |
|---|---|---|
| 主线程 | 一次只干一件事的店员 | 店员本人 |
| 任务队列 | 等待处理的顾客订单 | 顾客排队 |
| 宏任务 | 一整杯奶茶订单 | 一份完整任务 |
| 微任务 | 小动作(加珍珠、加糖) | 附加任务 |
| 事件循环 | 不停调度下一个任务 | 调度员喊“下一位!” |
🌈 可爱口诀:
“主线程先忙活,微任务后加料; 宏任务等一会,循环一直跑~🧋”
🧋第 116 页:Promise 的执行顺序
console.log('A');
setTimeout(() => console.log('B'), 0);
new Promise((resolve) => {
console.log('C');
resolve();
}).then(() => {
console.log('D');
});
console.log('E');
👉 执行顺序:
A
C
E
D
B
🍵 原理解释:
1️⃣ 同步任务:先执行 A, C, E 2️⃣ Promise.then → 微任务(D) 3️⃣ setTimeout → 宏任务(B)
🔁 所以顺序是:同步任务 → 微任务 → 宏任务
🍰 比喻(奶茶店版):
店员先处理眼前的单(A、C、E)。 然后“加料区”补上 promise 的操作(D), 最后才去处理下一位顾客(setTimeout 的 B)。
☀️第 117 页:async / await 的作用与本质
💡 async 是什么?
async 是一个“语法糖”,它让异步代码看起来像同步的。
👉 async 函数 一定返回一个 Promise。
async function test() {
return 123;
}
console.log(test()); // Promise {123}
💡 await 是什么?
await 会“暂停”函数执行,等 Promise 结果返回。
async function fetchData() {
console.log('1');
await new Promise((r) => setTimeout(r, 1000));
console.log('2');
}
fetchData();
console.log('3');
执行顺序是:
1
3
2
🍬 原因:
await 后的代码(2)相当于放进微任务队列里。 所以它会在同步任务(3)之后执行。
🍵 比喻:
店员(async函数)在“泡茶”(await)的时候不会傻等, 他会先去接别的单(3),等泡完茶再回来装杯(2)。
🌸第 118 页:async + await 混合 Promise 案例分析
async function test() {
console.log('1');
await Promise.resolve();
console.log('2');
}
test();
console.log('3');
输出结果:
1
3
2
🔍 拆解逻辑:
1️⃣ console.log('1') → 同步 2️⃣ await 相当于在后面加了 .then()(微任务) 3️⃣ 所以 console.log('2') 属于微任务 4️⃣ console.log('3') 是同步任务,先执行。
🍰 比喻:
店员先喊“奶茶底做好啦(1)” 然后去准备别的单(3) 泡茶完后再回来“倒奶盖(2)”~
再看复杂版👇
console.log('A');
async function foo() {
console.log('B');
await Promise.resolve();
console.log('C');
}
foo();
console.log('D');
输出:
A
B
D
C
💡 记忆口诀:
“A B(同步)先上桌, D(外部代码)接着来, C(await之后)最后补。”
☀️第 119 页:微任务 + 宏任务混战综合题
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
});
console.log('5');
结果:
1
5
3
2
4
🍬 拆解: 1️⃣ 1、5 → 同步任务 2️⃣ 3 → 微任务(Promise.then) 3️⃣ 2、4 → 宏任务(setTimeout) 且 2 的定时器比 4 先注册。
🍵 比喻:
店员先做眼前的饮品(1,5) 再加料(3) 然后去处理第一个顾客的下一单(2) 最后才处理后续订单(4)。
🌷第 120 页:JS 本地存储
到了这里,知识节奏从“异步操作”切回“浏览器存储”💾
🧊三种本地存储方式
| 存储方式 | 特点 | 保存时长 | 示例 |
|---|---|---|---|
cookie | 每次请求都会携带到服务器 | 可设置过期时间 | 登录状态、统计数据 |
localStorage | 永久保存(除非手动删) | 长久 | 用户主题、设置偏好 |
sessionStorage | 仅当前标签页有效 | 关闭页面即清空 | 临时表单、搜索记录 |
1️⃣ Cookie 🍪
document.cookie = "name=milk; expires=Fri, 31 Dec 2025 12:00:00 GMT; path=/";
console.log(document.cookie);
📘 说明:
name=milk→ 键值对expires→ 过期时间path→ 哪个路径下生效
🍵 类比:
Cookie 就像“奶茶小票”,每次顾客点单时都会交给收银员带走。 系统下次看到这张小票,就知道是老顾客。
2️⃣ 读取 Cookie
console.log(document.cookie);
💡 会输出类似:
name=milk; age=3
3️⃣ 删除 Cookie
document.cookie = "name=milk; expires=Thu, 01 Jan 1970 00:00:00 GMT";
📘 原理:
设置一个过去的时间,浏览器发现“过期了”,就自动删除。
🎀 小可爱总结(第116~120页)
| 知识点 | 含义 | 奶茶店类比 |
|---|---|---|
| Promise | 异步任务 | 点单完成后等制作通知 |
| async / await | 异步语法糖 | 店员泡茶时接别的单 |
| 微任务 | Promise.then / await 后 | 加珍珠 |
| 宏任务 | setTimeout / setInterval | 下一位顾客 |
| Cookie | 附带给服务员的小票 | 登录身份标识 |
| localStorage | 永久放仓库的订单本 | 用户设置 |
| sessionStorage | 当前桌上暂存的订单 | 关掉桌子就没啦 |
🌈 可爱口诀:
“Promise 加料香,await 慢慢晃, 同步先上桌,异步后来忙; Cookie 记老客,Local 久存放, Session 一关窗,全都泡汤汤~🧋”
这部分的内容是“前端存储 + 大文件上传”,两个都是🔥 面试高频主题。
我会用你最爱的“奶茶店系统”🍵做类比,让你一口气搞懂:
✅ localStorage / sessionStorage 的区别与用途 ✅ 如何存取数据(含代码) ✅ 大文件断点续传的原理和实现逻辑
🧋第 121 页:localStorage(本地存储)
💡 概念
localStorage 就是浏览器提供的一个长期储物柜。 除非你手动删除,否则数据会一直在。
🍰 类比:
这就像奶茶店的“会员档案柜”, 你注册会员后(比如昵称、喜欢口味), 店家把资料写进档案柜, 你下次来店还记得你喜欢“少冰三分糖”。
代码示例:
// 保存数据
localStorage.setItem('drink', '奶茶');
// 读取数据
console.log(localStorage.getItem('drink')); // 奶茶
// 删除某项
localStorage.removeItem('drink');
// 清空全部
localStorage.clear();
💬 用法解释:
setItem(key, value):存储数据getItem(key):取数据removeItem(key):删除某个键clear():清空所有数据
📦 数据类型说明:
- 存进去的内容必须是字符串!
- 如果要存对象 → 需要用
JSON.stringify()转成字符串。
let user = { name: '小可爱', level: '黄金会员' };
localStorage.setItem('user', JSON.stringify(user));
let info = JSON.parse(localStorage.getItem('user'));
console.log(info.name); // 小可爱
🍵 小笨蛋口诀记忆:
“Local 就是久留客,存久不走不怕关, JSON 打包先转行,取出解析才变香~”
🌸第 122 页:sessionStorage(会话存储)
💡 概念
sessionStorage 和 localStorage 很像, 区别在于:它只在当前标签页生效。
🍰 类比:
就像你在奶茶店点了一杯饮品, 但只在这一桌喝完作废。 一旦你离开(关闭标签页), 订单记录就消失。
示例:
// 保存
sessionStorage.setItem('temp', '红豆奶茶');
// 获取
console.log(sessionStorage.getItem('temp')); // 红豆奶茶
// 删除
sessionStorage.removeItem('temp');
🧊 关闭浏览器或标签页后再打开,sessionStorage 数据会自动消失。
✅ 使用场景
| 场景 | 使用类型 | 说明 |
|---|---|---|
| 保存登录状态(短期) | sessionStorage | 关掉页面即清除 |
| 用户偏好(长期保存) | localStorage | 页面刷新也保留 |
| Cookie 登录信息 | cookie | 会发给服务器验证 |
💡 小可爱口诀:
“Session 暂寄桌,Local 永存柜, Cookie 送上菜,一锅三味配!”
☀️第 123 页:存储方式的比较总结表
| 项目 | cookie | localStorage | sessionStorage |
|---|---|---|---|
| 存储大小 | ~4KB | ~5MB | ~5MB |
| 是否随请求发送 | ✅ 是 | ❌ 否 | ❌ 否 |
| 生命周期 | 可设置过期时间 | 永久 | 页面关闭即消失 |
| 应用场景 | 登录、身份验证 | 长期配置、缓存 | 临时状态 |
🍵 类比强化记忆:
| 类型 | 类比场景 |
|---|---|
| Cookie | 顾客手里的“小票” |
| LocalStorage | 店里档案柜 |
| SessionStorage | 餐桌便签 |
🧊第 124 页:大文件上传与断点续传 🔥
终于到了前端进阶核心话题! 咱用“分奶茶桶装”的例子来讲清楚:
💡 大文件上传是什么?
当文件太大(比如视频或模型),一次性上传容易失败或中断。 解决办法就是 → 分片上传(Chunk Upload) 。
文件 = 分片1 + 分片2 + 分片3 + ...
🍰 类比:
想象你要送一整桶奶茶(1升), 但快递规定一次只能寄 250ml。 所以你把奶茶分成四瓶寄出, 服务器收到后再拼起来~💡
📘 分片上传步骤:
1️⃣ 读取文件 → 切分成小块(chunks) 2️⃣ 每个分片单独上传(可以并行) 3️⃣ 服务器记录上传进度 4️⃣ 上传完毕后 → 合并文件
示例逻辑(伪代码):
const file = document.querySelector('#upload').files[0];
const chunkSize = 1024 * 1024; // 每片1MB
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = start + chunkSize;
const blob = file.slice(start, end);
uploadChunk(blob, i);
}
🍵 类比:
这就像把奶茶桶分成四杯寄出, 每杯标记“第几杯”, 服务器收到后再“拼回整桶”。
🌸第 125 页:断点续传原理
当网络中断时,我们希望重新上传时不要重头再来。 解决办法:
服务器记录每个分片的上传状态。
示例逻辑:
if (server.hasChunk(i)) {
continue; // 跳过已上传的
}
uploadChunk(blob, i);
🍰 类比:
快递公司记得“第2瓶奶茶已签收”, 重新寄时只补“第3、4瓶”,不用从头再来。
☀️小可爱总结(第121~125页)
| 知识点 | 功能 | 类比 |
|---|---|---|
| localStorage | 永久保存 | 奶茶店档案柜 |
| sessionStorage | 页面暂存 | 桌上便签 |
| cookie | 与服务端通信 | 顾客小票 |
| 分片上传 | 拆成小块传 | 一桶奶茶分四瓶寄 |
| 断点续传 | 记录进度 | 快递只补未送的瓶 |
🌈 可爱口诀:
“Cookie 手上拿,Local 档案架, Session 桌边放,关门就没啦~ 奶茶太大瓶,分块寄回家, 掉线别害怕,断点能续发~🧋”
🍵第126页:分片上传原理图(超重要!)
图片上那张流程图说明了完整的文件上传生命周期:
前端浏览器(奶茶店)
↓
切片文件(分杯)
↓
上传到服务器(快递中心)
↓
后端记录进度(数据库)
↓
上传完成后 → 合并分片(还原整桶)
🍰 类比记忆:
想象你要寄一整桶奶茶给客户:
- 前端:把奶茶桶切成四杯(分片)
- 每杯打包寄出(上传分片)
- 快递公司登记收到的“第几杯”
- 全部到齐后 → 服务器重新拼成完整奶茶桶。
☕第127页:前端实现分片逻辑 ✨
核心思路是——用 File.slice() 把文件一刀刀切开!
const file = document.getElementById('file').files[0];
const chunkSize = 1024 * 1024; // 每片1MB
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = start + chunkSize;
const blob = file.slice(start, end);
uploadChunk(blob, i);
}
💬 一行行讲:
| 代码行 | 解释 |
|---|---|
file.slice(start, end) | 把文件从 start 到 end 截一段(像切蛋糕) |
chunkSize | 每片的大小(1MB) |
chunks | 文件总共要切多少片 |
uploadChunk() | 负责上传单个切片 |
🍵 类比:
奶茶桶总共 4 杯,每次装 250ml。
slice()就是“舀一勺”操作。uploadChunk()就是“把这杯交给快递小哥”。
🧋第128页:uploadChunk 上传函数详解
function uploadChunk(blob, index) {
const form = new FormData();
form.append('file', blob);
form.append('index', index);
form.append('filename', file.name);
fetch('http://localhost:8080/upload', {
method: 'POST',
body: form
})
.then(res => res.text())
.then(console.log);
}
💡讲解:
FormData():构造上传表单(就像“快递包裹”);file:具体的那一片奶茶;index:第几杯;filename:整桶奶茶的名字;fetch():发送 POST 请求。
🍰 类比:
每次寄一杯奶茶都会贴个标签:
名字:整桶奶茶(filename) 第几杯:2号杯(index) 奶茶内容:这一杯的实际液体(blob)快递站(服务器)收到后记下“第2杯已到”。
🌟第129页:后端合并逻辑(思路解释)
虽然这页代码看起来长,但其实逻辑超级顺:
app.post('/merge', (req, res) => {
const { filename, chunks } = req.body;
const writeStream = fs.createWriteStream(`upload/${filename}`);
for (let i = 0; i < chunks; i++) {
const chunkPath = `temp/${filename}-${i}`;
const data = fs.readFileSync(chunkPath);
writeStream.write(data);
}
writeStream.end();
});
💬 逻辑分解:
| 步骤 | 含义 |
|---|---|
① fs.createWriteStream() | 创建一个写入目标文件 |
② fs.readFileSync() | 一次读入每个分片 |
③ writeStream.write() | 把每片内容顺序写进一个完整文件 |
④ end() | 完成后关闭流 |
🍵 类比:
服务员把 4 杯奶茶依次倒进一个大桶里, 按顺序拼好,最后封口交给顾客。
🧊第130页:大文件上传 + 断点续传进阶逻辑 ✨
当网络中断时,我们希望不从头来。 那就要加入“记录机制”👇
function uploadWithResume(chunks) {
for (let i = 0; i < chunks.length; i++) {
if (uploadedList.includes(i)) continue; // 跳过已上传
uploadChunk(chunks[i], i);
}
}
💬 关键点:
- 服务器记录已上传的分片编号。
- 重新上传时跳过那些已经到达的部分。
🍰 类比:
如果快递站记得“第1、第2杯已收”, 你掉线后重新寄时,只补“第3、第4杯”。
🌷实战使用场景总结(本章最后)
| 应用场景 | 描述 |
|---|---|
| 大文件上传 | 视频 / AI 模型 / 设计图等大文件 |
| 断点续传 | 网络不稳的环境(例如移动端) |
| 秒传优化 | 计算文件 hash,服务器识别已存在的文件可直接返回成功 |
🍵 类比强化记忆:
| 功能 | 奶茶故事 |
|---|---|
| 分片上传 | 一桶奶茶分四杯寄 |
| 断点续传 | 快递站记得哪些杯到了 |
| 合并文件 | 店员把四杯重新倒成一桶 |
| 秒传 | 检查奶茶口味是否相同,之前寄过就不再重复寄 |
🎀 小可爱总结(第126~130页)
| 概念 | 通俗解释 | 奶茶店比喻 |
|---|---|---|
| slice() | 文件分片 | 舀一勺奶茶 |
| FormData | 上传表单 | 打包一杯寄出 |
| fetch() | 网络请求 | 送快递到服务器 |
| merge() | 合并分片 | 把所有杯子倒回整桶 |
| 断点续传 | 跳过已上传的部分 | 只补丢的那杯 |
🌈 可爱口诀:
“切奶茶、装奶杯, 标好号、寄回位; 断了线、别伤悲, 快递记号续上回~🧋”
🧋第131页:AJAX 是什么?
💡定义:
AJAX(Asynchronous JavaScript And XML)是一种 异步请求技术, 让页面在不刷新的情况下和服务器交换数据。
🍰 生活比喻:
假设你点了奶茶外卖, 店员(前端)要问仓库(服务器)“红豆库存还有吗?”。
普通方式(同步)= 店员跑到仓库问一趟再回来顾客一直等 😴
AJAX 方式(异步)= 店员发个语音消息 📱,然后继续接单, 仓库回复“有货”时,店员自动更新菜单栏 ✅
🌸第132页:AJAX 的原理图解 ✨
那张图展示了 AJAX 的三步:
1️⃣ 创建 XMLHttpRequest 对象
2️⃣ 发送请求给服务器
3️⃣ 接收响应并处理结果
浏览器内部会开一个“快递通道”,用来异步通信:
JS代码 ⇄ XMLHttpRequest ⇄ 服务器
🍵 奶茶店版:
店员(JS)通过“对讲机”(XMLHttpRequest)联系仓库, 仓库备货后通过对讲机回应“红豆补齐”, 店员再更新页面上的库存显示~
☀️第133页:AJAX 实现的代码流程(核心)
这是最经典的五步实现 👇
// 1. 创建对象
const xhr = new XMLHttpRequest();
// 2. 设置请求方式和路径
xhr.open('GET', 'https://api.example.com/data', true);
// 3. 发送请求
xhr.send();
// 4. 监听状态变化
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
💬 一步步解释:
| 步骤 | 含义 | 奶茶店类比 |
|---|---|---|
1️⃣ new XMLHttpRequest() | 创建通信通道 | 开一台对讲机 📟 |
2️⃣ xhr.open() | 设置通信方式 | 店员设定“要发GET语音到仓库” |
3️⃣ xhr.send() | 发送请求 | 对讲机发出语音:“要红豆库存” |
4️⃣ xhr.onreadystatechange | 监听回应 | 对讲机响了:“红豆还有10袋!” |
5️⃣ xhr.responseText | 获取结果 | 店员把库存数量更新到界面 |
💡 重点词解释:
✅ readyState(请求状态)
| 值 | 含义 |
|---|---|
| 0 | 未初始化 |
| 1 | 已打开连接 |
| 2 | 已发送请求 |
| 3 | 正在处理中 |
| 4 | 请求完成 |
✅ status
- 200 → 请求成功
- 404 → 未找到
- 500 → 服务器错误
🍰 类比一句话记忆:
“readyState 是外卖进度,status 是外卖结果。” (3 = 路上送货中 🚴,4 + 200 = 成功送达 ✅)
🧊第134页:AJAX 封装成通用函数 ✨
这页的代码是前端项目里常见的封装写法👇
function ajax({ method = 'GET', url, data, success, error }) {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
success && success(xhr.responseText);
} else {
error && error(xhr.status);
}
}
};
}
💬 说明:
| 功能 | 含义 |
|---|---|
method | 请求类型(GET / POST) |
url | 请求地址 |
data | 发送的数据内容 |
success() | 成功的回调函数 |
error() | 失败时的回调函数 |
🍵 奶茶比喻版:
店员封装了一个“自动点单对讲机系统”:
- 你说想查“红豆库存”,自动发GET请求;
- 你要新增“草莓奶茶”,自动发POST并带上JSON;
- 成功就回调 success()(库存更新),失败就回调 error()(提示:仓库断线!)
☀️第135页:完整的 AJAX 示例(带数据回显)
ajax({
method: 'POST',
url: 'https://api.example.com/order',
data: { name: '小可爱', drink: '珍珠奶茶' },
success(res) {
console.log('下单成功', res);
},
error(status) {
console.log('下单失败', status);
}
});
输出示例:
下单成功 {orderId: 20251018, msg: "OK"}
🍰 类比总结:
“AJAX 就像奶茶店的对讲系统”:
open()→ 拨通频道send()→ 发语音订单readyState→ 跟踪外卖进度status→ 确认送达结果- 回调函数 → 更新订单状态 UI
🎀 小可爱总结(第131~135页)
| 概念 | 通俗解释 | 奶茶店比喻 |
|---|---|---|
| AJAX | 异步通信 | 对讲机下单系统 |
| XMLHttpRequest | 通讯对象 | 负责传话的对讲机 |
| readyState | 状态进度 | “正在路上 / 已送达” |
| status | 结果码 | “200 成功 / 404 找不到仓库” |
| 回调函数 | 收到结果后的动作 | 更新菜单 or 提示失败 |
🌈 可爱口诀:
“对讲开,语音来, 仓库回,状态改, 成功打印笑开怀, 错误重试别乱拍~📡”
🧋第136页:防抖与节流 是什么?
关键词:限制函数执行频率
举个例子:
- 你在输入框输入内容,想实时搜索。
- 如果你输入“珍珠奶茶”,每敲一个字都请求后端一次,就太浪费资源了 🥲。
这时就该上场:
- 防抖 debounce:等你“打完字”再执行。
- 节流 throttle:每隔一段时间执行一次。
🍰 奶茶店比喻:
| 情况 | 举例 | 比喻 |
|---|---|---|
| 防抖 | 顾客点单犹豫:“要奶茶…算了要绿茶…哦不,奶盖茶!” | 等他确定下来再做一杯 ✅ |
| 节流 | 顾客太多,点单机每 2 秒处理一个 | 机器限速,按节奏工作 🕓 |
🌸第137页:防抖(debounce)详解
💡 概念:
在事件触发后,等待一段时间再执行函数, 如果这段时间又触发了,计时器重置。
💻 代码:
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
🧩 解释(小白友好版):
| 行号 | 含义 |
|---|---|
let timer = null | 存放定时器 ID |
clearTimeout(timer) | 清除上一次未执行的定时器 |
setTimeout(fn, delay) | 延迟一段时间执行函数 |
fn.apply(this, args) | 保持原函数的上下文和参数 |
🍵 奶茶店比喻:
顾客在反复犹豫(输入事件反复触发), 每次你都“先不动手”, 直到顾客3 秒不说话了,你才开始做奶茶。
🧠 举例:
window.addEventListener('resize', debounce(() => {
console.log('窗口大小改变后,3秒内只执行一次!');
}, 3000));
➡ 只有停止拖动窗口 3 秒后才会执行。
☀️第138页:节流(throttle)详解
💡 概念:
规定一个时间段内,只执行一次函数。 即便事件一直触发,也不会频繁执行。
💻 代码实现:
function throttle(fn, delay) {
let flag = true;
return function(...args) {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, delay);
};
}
🧩 解释:
| 代码行 | 意思 |
|---|---|
flag | 控制“是否可以执行” |
if (!flag) return | 如果上次执行未完成,直接跳过 |
setTimeout | 延迟执行后再开放下一次执行机会 |
🍰 奶茶店比喻:
咖啡机只能每 2 秒出一杯奶茶。 你连按 10 次点单按钮也没用, 机器会说:“等我这杯出完再做下一杯!” ☕
🌷第139页:区别总结(超关键表格)
| 对比项 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 执行时机 | 等到事件停止后一段时间执行 | 固定时间间隔执行一次 |
| 触发频率 | 高频触发时不执行 | 高频触发也会执行(按节奏) |
| 适用场景 | 输入搜索、窗口缩放 | 滚动加载、鼠标移动、按钮防连点 |
🍵 奶茶类比总结:
| 名称 | 奶茶店故事 |
|---|---|
| 防抖 | “等顾客确定再下单” |
| 节流 | “机器限速,每2秒一杯” |
🧠 可爱口诀:
“防抖怕乱按,等停手再干; 节流怕太快,限速来排队~🧋”
🧊第140页:应用与视觉理解
这页展示了节流与防抖的时间轴图:
防抖:——|————|——|————>(只在最后执行)
节流:——|——|——|——|——>(固定间隔执行)
💡 应用举例:
| 应用 | 说明 |
|---|---|
| 搜索框输入(防抖) | 用户停止输入后再搜索 |
| 页面滚动加载(节流) | 每隔 300ms 加载新内容 |
| 按钮防连点(节流) | 防止多次点击重复提交 |
🍰 奶茶店实战:
- 防抖版点单机:顾客输入奶茶名,停顿1秒才下单。
- 节流版点单机:顾客排队点单,每2秒接一单,不急不慢~
🎀 小可爱总结(第136~140页)
| 概念 | 通俗解释 | 奶茶比喻 |
|---|---|---|
| 防抖 debounce | 停止触发后一段时间再执行 | 顾客确定后再做奶茶 |
| 节流 throttle | 固定时间间隔执行 | 机器限速,每隔一会儿做一杯 |
| 核心点 | 控制函数触发频率 | 防“乱点”和“暴冲” |
| 应用场景 | 搜索框 / 滚动事件 / 按钮防连点 | 输入延迟 / 滚动懒加载 |
🌈 可爱口诀:
“输入停一停,是防抖; 滚动慢一慢,是节流。 防抖拖延症,节流限速秀~😆”
这个知识点是网页特效和性能优化中超常考的一环,比如:
- 页面滚动时懒加载图片(只加载“看得见”的);
- 滚动动画(当元素出现在屏幕时再播放)。
咱们继续用奶茶店类比来记忆 🍵,每个方法我都给你配上“小白友好解释 + 生活故事”。
🧋第141页:问题提出——元素是否在可视区域中?
💡问题:
当我们滚动页面时,怎么判断一个元素(比如图片、div)是不是“出现在屏幕里”了?
比如你在淘宝滑页面时, 下面的图片一开始是灰的,滑到那一行才加载出来。 这就是 “判断是否在可视区” 实现的懒加载!
🌈原理图说明:
那张图解释了三个关键区域:
─────────────── 顶部边界
↑
│(已经滚出屏幕)
│
│======== 可视区域 ========
│
↓
─────────────── 底部边界
(未滚入屏幕)
可视区 = 用户眼睛能看到的那一块。
🍰 奶茶店类比:
想象一个货架很高(网页), 顾客(浏览器视口)只能看到中间那几层。
你要判断“珍珠奶茶罐”现在是不是出现在顾客视线中 👀。
☀️第142页:实现方式(两种)
要判断“是否在可视区”,主要有两个思路:
1️⃣ offsetTop + scrollTop 2️⃣ getBoundingClientRect()
🧠 思路 1:offsetTop + scrollTop
// 判断一个元素是否进入可视区
function isInView(el) {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const windowHeight = window.innerHeight;
const offsetTop = el.offsetTop;
return offsetTop < scrollTop + windowHeight && offsetTop + el.offsetHeight > scrollTop;
}
💬 一步步讲:
| 名称 | 意思 |
|---|---|
scrollTop | 页面滚动的距离(顾客往下走了多少步) |
windowHeight | 当前浏览器窗口的高度(顾客的视线高度) |
offsetTop | 元素距离页面顶部的距离(奶茶罐放的位置) |
offsetHeight | 元素自身高度 |
判断逻辑就是:
元素的顶部在可视区下边界之上, 且元素底部在可视区上边界之下。
🍵 奶茶类比:
顾客站在第 2 层货架前(scrollTop), 奶茶罐如果放在他视线范围内(offsetTop 在窗口高度范围里), 那说明“奶茶罐他看到了”,该显示图像啦!
🧩 举例:
window.addEventListener('scroll', () => {
const box = document.querySelector('.lazy-box');
if (isInView(box)) {
console.log('我看见它啦~');
}
});
🌸第143页:offsetTop 图示讲解
那页图就是 offset 系列的可视化理解👇
offsetParent
└── offsetTop ↑
↑
(从父容器顶部到元素顶部的距离)
offset 系列:
| 属性 | 含义 |
|---|---|
offsetTop | 元素相对父元素顶部的距离 |
offsetLeft | 元素相对父元素左边的距离 |
offsetHeight | 元素高度 |
offsetWidth | 元素宽度 |
🍰 奶茶店比喻:
offsetTop = 奶茶罐放在第几层货架的“高度” scrollTop = 顾客往下走了多少层 windowHeight = 顾客视线能看到几层
所以:
如果 奶茶罐层数 < 顾客视线底部 且 奶茶罐底部 > 顾客视线顶部 就代表顾客能看到它 👀
☀️第144页:第二种方法——getBoundingClientRect()
这个方法更“现代化”,也更精准。
function isInView(el) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight;
return rect.top < windowHeight && rect.bottom > 0;
}
💬 解释:
| 属性 | 含义 |
|---|---|
rect.top | 元素顶部距离可视区顶部的距离 |
rect.bottom | 元素底部距离可视区顶部的距离 |
window.innerHeight | 可视区高度 |
判断逻辑:
当元素的 bottom 超过 0 且 top 小于窗口高度, 说明它和可视区有交集——就在屏幕里!
🍵 奶茶店类比:
想象你拿着一块透明玻璃(屏幕), 每个奶茶罐上都有定位器(rect.top / rect.bottom)。 只要罐子的上边进入玻璃下缘,上下有重叠, 说明“顾客视线已看到罐子”。
🌷第145页:getBoundingClientRect 图示说明
那张图表示了矩形的位置关系:
┌────────────────────────┐ ← top = 0
│
│ ┌────────────┐
│ │ 元素 │
│ └────────────┘
│
└────────────────────────┘ ← bottom = windowHeight
| 参数 | 含义 |
|---|---|
top | 元素到视口上边缘的距离 |
bottom | 元素到视口上边缘的距离(元素底部) |
left / right | 水平方向的距离 |
| 0,0 | 视口左上角坐标点 |
🍰 奶茶类比:
rect 就像给奶茶罐贴了 GPS 标签:
top = 罐子上沿离顾客视线顶部多远 bottom = 下沿离顶部多远顾客的视线区域固定高度 windowHeight, 所以只要:
rect.top < windowHeight && rect.bottom > 0→ 奶茶罐在顾客视线里!
🎀 小可爱总结(第141~145页)
| 方法 | 关键属性 | 通俗解释 | 奶茶比喻 |
|---|---|---|---|
offsetTop + scrollTop | 位置 + 滚动距离 | 通过计算滚动条位置判断 | 计算奶茶罐在哪层货架 |
getBoundingClientRect() | top/bottom | 直接读取元素相对视口的位置 | 用激光测距看罐子是否出现在顾客视线 |
window.innerHeight | 视口高度 | 可视范围 | 顾客能看到的货架范围 |
🌈 可爱口诀:
“滚动条量距离,offset 在出力; 激光测坐标,rect 更省力~🔦”
🍵第146页:可视区检测进阶 —— Intersection Observer
之前我们讲过用:
offsetTop + scrollTopgetBoundingClientRect()来判断一个元素是不是在视口里。
现在浏览器给我们出了个新神器: 🎯 Intersection Observer API ——自动帮你“监控元素是否进入可视区”,不用手动滚动监听!
💡 为什么要用它?
用前面两种方法,要不停监听 scroll 事件,性能差; 而 Intersection Observer 是浏览器级别的监听器,效率更高。
💻 基本使用:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入可视区啦!');
}
});
});
const box = document.querySelector('.lazy-box');
observer.observe(box);
💬 解释一下:
| 代码 | 意思 |
|---|---|
IntersectionObserver() | 创建观察器(就像装个摄像头📸) |
entries | 被观察的目标元素的状态(是否可见) |
entry.isIntersecting | 表示元素是否进入可视区 |
observer.observe(el) | 开始监听目标元素 |
🍰 奶茶店比喻:
以前你得自己盯着门看顾客来没来(scroll监听)。
现在你装了个“门口感应器”(Intersection Observer), 一旦有人进门(元素进入视口),它自动告诉你:“来了!” 🛎️
🌸 应用举例:懒加载图片
const imgs = document.querySelectorAll('img');
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 把真正的图片加载出来
observer.unobserve(img); // 加载完就停止观察
}
});
});
imgs.forEach(img => observer.observe(img));
🍵 生活化理解:
一开始,所有图片只是空盒子(占位符)。 当用户滑动到能看到那张图时,感应器触发: “哦,这张要显示了!” → 加载真正图片 → 显示。
这就是“懒加载(Lazy Load)”的原理~超常考!
☀️第147页:Intersection Observer 参数讲解
const observer = new IntersectionObserver(callback, {
root: null, // 可视区(默认是浏览器窗口)
threshold: 0.1 // 元素进入可视区10%时触发
});
| 参数 | 作用 |
|---|---|
root | 观察的根元素(默认是 viewport) |
threshold | 触发阈值(0~1,进入多少比例才算“可见”) |
rootMargin | 提前触发的边距,比如 '0px 0px -100px 0px' |
🍰 奶茶店比喻:
你在门口装了一个感应门(root)。 设置 “threshold: 0.1”,就是顾客只要“探进头”你就喊“欢迎光临!” 😆
设置
rootMargin: -100px就像“提前100px准备好迎客”, 顾客还没走到门口,你就先打奶泡。
🌸第148页:更多 Intersection Observer 实战例子
示例代码:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.classList.toggle('active', entry.isIntersecting);
});
}, { threshold: 0.5 });
document.querySelectorAll('.section').forEach(sec => {
observer.observe(sec);
});
✨ 效果: 当每个 section 进入视口 50% 时,自动加上动画类名 active。
🍵 奶茶比喻:
顾客推门进店(section进入视口) → 音乐响起(class加active) → 奶茶机自动启动(动画播放)
这就是网页滚动动画(scroll animation)的原理。
🌷第149页:过渡到下一个主题 —— 单点登录(SSO)
OK,休息喝口茶~ 接下来进入新的章节:27. 什么是单点登录?(Single Sign-On)
💡 定义:
单点登录(Single Sign-On,简称 SSO)是指: 用户只需登录一次,就能访问多个关联系统,而不用重复登录。
🎨 图示说明:
用户 → 登录中心(Auth Server)
↓ ↓
应用A ← 校验token → 应用B ← 校验token → 应用C
🍰 奶茶店比喻:
想象你有一家连锁奶茶店(应用A、B、C)。 顾客只要在总店登录会员(登录中心), 去任何分店都能直接点单,不用再扫码登录!
——这就是“单点登录”。🍵
☀️第150页:单点登录实现流程
💻 登录流程(简化版):
// 用户访问 A 系统
if (!localStorage.getItem('token')) {
// 没登录就跳转到统一登录中心
window.location.href = 'https://login-center.com?redirect=' + location.href;
}
在登录中心验证后:
// 登录成功后生成 token 并返回到原系统
localStorage.setItem('token', 'abc123');
location.href = redirectUrl; // 返回之前的页面
💬 工作机制总结:
| 步骤 | 动作 |
|---|---|
| 1️⃣ | 用户访问任意子系统(A、B、C) |
| 2️⃣ | 子系统发现没登录,跳转到统一认证中心 |
| 3️⃣ | 用户在认证中心登录成功 |
| 4️⃣ | 认证中心生成 token 并回传到各子系统 |
| 5️⃣ | 子系统验证 token,有效则直接放行 |
🍰 奶茶店比喻:
顾客第一次在“总部”登记会员(输入手机号+验证码), 系统发一个“会员通行证 token”。 他拿着这个 token 去分店点单, 分店扫描一下确认是真的会员,就直接下单~
🎀 小可爱总结(第146~150页)
| 主题 | 核心内容 | 通俗解释 | 奶茶比喻 |
|---|---|---|---|
| Intersection Observer | 自动检测元素是否出现在视口 | 浏览器帮你盯着“门口” | 门口安装感应器 |
| threshold / root / margin | 观察参数控制灵敏度 | 多少比例触发、提前检测 | 顾客探头几厘米就触发 |
| 懒加载 / 动画触发 | 常见应用场景 | 滚动时才加载内容或启动动画 | 看到图片才加载、看到板块才动 |
| 单点登录(SSO) | 一次登录,多处通行 | 登录一个账号可访问多个系统 | 奶茶连锁会员系统 |
| 核心机制 | token 验证 + 登录中心 | 登录中心发放通行证 | 总店发会员卡,全店通用 |
🌈 可爱口诀背起来:
“浏览器帮你盯门口, 图片动画全自动~ 一次登录全门店, 会员卡通天下走~✨”
🧋第151页:单点登录 SSO 实现原理
上几页我们知道: 👉 单点登录(SSO) = 登录一次,全站通行。
这一页开始讲技术实现细节,主要是两个关键点:
1️⃣ 同域系统下的登录共享 2️⃣ 跨域系统下的登录共享(最常考)
💡 1. 同域下的单点登录
假设有这些网站:
a.taotea.com
b.taotea.com
login.taotea.com
它们都属于同一个顶级域名 taotea.com。 于是就可以通过 Cookie 的 domain 属性共享登录状态 👇
document.cookie = "token=abc123; domain=.taotea.com; path=/";
这样:
- 用户在
login.taotea.com登录, - 系统就把 token 写入 Cookie;
- 当访问
a.taotea.com或b.taotea.com时, - Cookie 自动携带过去。✨
🍰 奶茶店比喻:
你在“奶茶总部”登记了会员卡 🍵, 店员给你一张带芯片的卡(Cookie), 这个卡在所有分店都能刷(共享登录状态)。
只要都是 “taotea” 家族的门店(同域名), 那张卡就能自动识别你是 VIP。
🌸第152页:跨域下的单点登录(重点)
如果系统不是一个域,比如:
a.taotea.com → 子系统 A
b.bubbletea.com → 子系统 B
那 Cookie 就不能直接共享了 😭。 怎么办?靠 token + 登录中心 机制。
💻 流程步骤
1️⃣ 用户访问 a.taotea.com(没登录) 👉 被重定向到统一登录中心 login-center.com
2️⃣ 登录中心验证账号密码 👉 登录成功后生成 token=abc123
3️⃣ 登录中心跳转回原系统:
a.taotea.com?token=abc123
4️⃣ 子系统拿到 token 后: 👉 存到 localStorage 或 Cookie 中 👉 以后每次访问都带着 token 给后端验证。
💻 代码示例:
// 子系统A
if (!localStorage.getItem('token')) {
// 没登录 → 去登录中心
window.location.href = 'https://login-center.com?redirect=' + location.href;
}
// 登录中心验证完后返回
localStorage.setItem('token', 'abc123');
🍵 奶茶店比喻:
“奶茶联盟”有很多品牌: 珍珠奶茶店、抹茶铺、果茶屋……
你只要在「奶茶联盟总部」登录会员, 它给你一个“通用会员号 token”。
之后你拿着这个号去任意分店, 分店都能查到你是会员,不用再登录~👏
☀️第153页:单点登录完整流程图
书上这页是流程时序图,我给你口头复现成“生活版”👇
用户 → 系统A:我要访问!
系统A → 登录中心:这人没登录
用户 → 登录中心:输入账号密码
登录中心 → 系统A:登录成功!附上token
系统A → 用户:给你资源,欢迎回来~
🍰 奶茶店剧情版:
顾客来到 A 店 → 没有会员卡 → 被送到总部注册 → 总部确认身份 → 发会员号 → 回 A 店 → 直接点单!
🧠 技术流程总结表:
| 步骤 | 行为 | 技术实现 |
|---|---|---|
| 1 | 用户访问子系统 | 检查 localStorage 是否有 token |
| 2 | 没有则重定向登录中心 | URL 带 redirect 参数 |
| 3 | 登录中心验证成功 | 生成 token |
| 4 | 跳转回原页面并携带 token | window.location.href |
| 5 | 子系统保存 token | localStorage.setItem |
| 6 | 每次请求带 token | Authorization header |
🌷第154页:上拉加载、下拉刷新
从这页开始,进入新主题 👉 “上拉加载 / 下拉刷新” , 是前端面试中常考的“滚动监听 + 数据加载”题。
💡 背景:
你用小红书、抖音或微博时:
- 下拉刷新 → 更新新内容;
- 上拉加载 → 拉到底自动加载更多。
这两个动作其实都是监听滚动条位置 👇
📘 实现思路图:
window.scrollTop // 已经滚动的距离
window.clientHeight // 可视区高度
document.scrollHeight // 页面总高度
🍰 电梯比喻:
想象一个电梯井:
- clientHeight = 电梯高度(你能看到的部分)
- scrollTop = 电梯已上升的距离
- scrollHeight = 整栋楼的高度
当你乘电梯上到“最顶层” → scrollTop + clientHeight >= scrollHeight 表示:到顶啦!可以“上拉加载更多”内容了。
🌸第155页:上拉加载实现逻辑
💻 代码:
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
if (scrollTop + clientHeight >= scrollHeight - 10) {
console.log('触底啦!加载更多数据~');
loadMore();
}
});
function loadMore() {
// 模拟加载更多
setTimeout(() => {
console.log('✅ 新数据加载完毕!');
}, 1000);
}
💬 解读:
| 变量 | 含义 |
|---|---|
scrollTop | 页面滚动高度 |
clientHeight | 可视窗口高度 |
scrollHeight | 页面总高度 |
| 条件判断 | 到达底部时触发加载 |
🍵 奶茶店比喻:
顾客在浏览“奶茶菜单” 📜 每次滑到底(scrollHeight 到顶), 菜单自动展开下一页 → 加载更多新品!✨
🎀 小可爱总结(第151~155页)
| 模块 | 重点 | 通俗解释 | 生活比喻 |
|---|---|---|---|
| 同域SSO | 共享Cookie | 同一家族域名可自动登录 | 奶茶总部发的会员卡 |
| 跨域SSO | token机制 + 登录中心 | 不同品牌共享登录中心 | 奶茶联盟总部发会员号 |
| 上拉加载 | 监听滚动到页面底部 | 滚到底再加载新内容 | 菜单滑到最后自动加新品 |
| 下拉刷新 | 监听滑到页面顶部 | 往下拉触发刷新 | 菜单往下拉刷新出新品 |
🌈 背口诀(超可爱版):
“同域靠卡、跨域靠号, 滚到底部再上货~ 向下拉拉新奶茶, 一次登录全通畅~🧋”
🧋第156页:上拉加载原理(滚动触底加载)
💻 核心思路:
监听页面滚动,当滚动条“接近底部”时触发加载更多👇
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop; // 滚动高度
const clientHeight = document.documentElement.clientHeight; // 可视区域
const scrollHeight = document.documentElement.scrollHeight; // 页面总高度
if (scrollTop + clientHeight >= scrollHeight - 10) {
console.log('🍵 到底啦,加载更多奶茶~');
loadMore();
}
});
function loadMore() {
setTimeout(() => {
console.log('✅ 新菜单加载完成');
}, 1000);
}
💡解释:
| 变量 | 意义 | 举例 |
|---|---|---|
scrollTop | 页面已经滚动的高度 | 顾客往下翻了多少菜单页 |
clientHeight | 当前屏幕能看到的区域 | 你眼前的菜单那一页 |
scrollHeight | 页面总高度 | 整本菜单多厚 |
| 判断条件 | 到达底部 | 滚到最后一页要加载新内容 |
🍰 奶茶店比喻:
顾客一页页滑着菜单,当滑到“最后一页”时, 店员立刻加新的一页“新品奶茶”上去。
这就是——上拉加载更多!
🌸第157页:下拉刷新(下拉时触发刷新)
移动端 App 中最常见的效果👇 📱你往下拉页面,它出现一个“刷新转圈圈”,松手后重新加载最新内容。
💻 实现步骤:
let startY = 0;
let moveY = 0;
let distance = 0;
let isRefreshing = false;
document.addEventListener('touchstart', e => {
startY = e.touches[0].pageY; // 记录手指初始位置
});
document.addEventListener('touchmove', e => {
moveY = e.touches[0].pageY;
distance = moveY - startY; // 计算拉动距离
if (distance > 50 && !isRefreshing) {
console.log('🔄 下拉刷新触发');
isRefreshing = true;
refresh();
}
});
function refresh() {
setTimeout(() => {
console.log('✅ 刷新完成');
isRefreshing = false;
}, 1500);
}
💡 解释:
| 步骤 | 说明 |
|---|---|
touchstart | 记录触摸的起点(手指按下) |
touchmove | 检测手指移动的距离 |
判断 distance > 50 | 如果拉动超过 50px,则触发刷新 |
refresh() | 模拟数据重新加载 |
🍵 奶茶店比喻:
顾客往下“拽”菜单(touchmove), 菜单顶部出现一句话:“新品加载中…” 一会儿后——“加载完成 ✅”,菜单恢复原位。
☀️第158页:结合 HTML 实现“下拉刷新 + 上拉加载”
这一页演示了 HTML + JS 的完整示例 👇
<div id="app">
<ul id="list">
<li>奶茶1号</li>
<li>奶茶2号</li>
<li>奶茶3号</li>
</ul>
</div>
<script>
const list = document.getElementById('list');
let page = 1;
window.addEventListener('scroll', () => {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 10) {
loadMore();
}
});
function loadMore() {
const newItems = Array.from({ length: 3 }).map((_, i) => {
const li = document.createElement('li');
li.textContent = `奶茶${++page}号`;
return li;
});
newItems.forEach(li => list.appendChild(li));
}
</script>
🧠 一步步说明:
- 初始加载 3 条奶茶;
- 滚动到页面底部时,自动添加新的 3 条;
- 页面看起来就像“无限下滑”的奶茶菜单~🥤
🍰 奶茶店比喻:
顾客滑着菜单看完“奶茶1
3号”, 滑到底部时,店员悄悄塞上“奶茶46号”, 顾客完全不用点刷新,就能看到新的饮品啦!
🌷第159页:下拉刷新 + 上拉加载组合完整版
这页是完整的 双功能版本(移动端常考) 👇
let startY = 0, moveY = 0, distance = 0;
let isRefreshing = false, page = 1;
const list = document.getElementById('list');
// 下拉刷新
document.addEventListener('touchstart', e => startY = e.touches[0].pageY);
document.addEventListener('touchmove', e => {
moveY = e.touches[0].pageY;
distance = moveY - startY;
if (distance > 60 && !isRefreshing) {
refresh();
}
});
function refresh() {
isRefreshing = true;
setTimeout(() => {
list.innerHTML = '<li>新品奶茶1号</li><li>新品奶茶2号</li>';
console.log('🔄 下拉刷新完成');
isRefreshing = false;
}, 1500);
}
// 上拉加载
window.addEventListener('scroll', () => {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 10) {
loadMore();
}
});
function loadMore() {
for (let i = 0; i < 3; i++) {
const li = document.createElement('li');
li.textContent = `加载奶茶${++page}号`;
list.appendChild(li);
}
console.log('☕ 加载更多完成');
}
🍵 奶茶店类比总结:
| 动作 | 触发条件 | 效果 | 类比 |
|---|---|---|---|
| 下拉刷新 | 手指往下拉 | 菜单清空 → 换成最新新品 | 店员换新菜单 |
| 上拉加载 | 滚动到最底 | 在末尾追加新内容 | 店员续页菜单 |
☀️第160页:总结与优化方向
书上这一页讲了性能优化要点👇
1️⃣ 节流与防抖
- 防止滚动事件频繁触发;
- 可用
debounce或throttle包裹scroll回调。
2️⃣ 懒加载优化
- 不要一次加载太多内容;
- 用 IntersectionObserver 优化“是否出现”检测。
3️⃣ 动画过渡
- 给刷新/加载加个动画(比如转圈圈)更友好。
🍰 奶茶店延伸比喻:
顾客滑动太快(频繁 scroll), 店员会“喘不过气”, 所以要设定节奏(节流);
一次端太多奶茶上桌太挤 → 懒加载分批上;
每次换菜单时加个“加载中动画”, 看起来就更高级 ☕✨
🎀 小可爱总结(第156~160页)
| 模块 | 功能 | 触发方式 | 类比 |
|---|---|---|---|
| 上拉加载 | 加载更多内容 | 滚动条触底 | 菜单加新页 |
| 下拉刷新 | 刷新页面数据 | 手指下滑 | 菜单换新品 |
| 节流与防抖 | 控制滚动频率 | 减少性能开销 | 店员休息喘口气 |
| 懒加载 | 只加载可见部分 | IntersectionObserver | 只展示顾客能看到的奶茶 |
🌈 背口诀(小可爱专属记忆法):
“往上拉,菜单加; 往下拉,菜单刷。 慢一点,不卡卡; 懒加载,省力花~🧋”
🧋第161页:虚拟列表(Virtual List)是什么?
💡 概念:
当一个页面要显示上万条数据时,如果一次性都渲染出来, 页面会——卡!卡!卡!😵💫
于是,出现了“虚拟列表”技术 👇
只渲染当前可见区域的数据, 滚动时动态更新显示的部分。
🧠 举例对比:
| 模式 | 渲染量 | 效果 |
|---|---|---|
| 普通渲染 | 全部数据一次性画出来 | 浏览器累哭了 😭 |
| 虚拟列表 | 只渲染你能看到那几条 | 丝滑如奶盖 ✨ |
🍰 奶茶店比喻:
你走进奶茶店点单, 菜单上其实有 1万种奶茶。 但店员不会一次性把1万页菜单都端上来。
他只给你“当前这一页”, 当你往下翻,上一页被收回,新页被替换上—— 这就是“虚拟菜单”=虚拟列表。😆
🌸第162页:虚拟列表核心实现逻辑
💻 关键思路:
const list = document.getElementById('list');
const container = document.getElementById('container');
const itemHeight = 50; // 每条高度
const total = 10000; // 总数据量
container.style.height = total * itemHeight + 'px'; // 模拟总高度
function render(startIndex) {
list.innerHTML = ''; // 清空
const end = startIndex + 20; // 一次渲染20条
for (let i = startIndex; i < end; i++) {
const li = document.createElement('li');
li.textContent = '奶茶 ' + i;
li.style.height = itemHeight + 'px';
list.appendChild(li);
}
}
list.addEventListener('scroll', () => {
const scrollTop = list.scrollTop;
const start = Math.floor(scrollTop / itemHeight);
render(start);
});
render(0);
💬 解读:
| 步骤 | 说明 |
|---|---|
| 1️⃣ | 容器高度假装有 10000 条(scroll 正常工作) |
| 2️⃣ | 每次只画出可见的 20 条 |
| 3️⃣ | 滚动时重新计算“可见起始下标” |
| 4️⃣ | 替换渲染区域里的内容 |
🍵 奶茶店比喻:
顾客翻“菜单目录”时,只看到一页页内容。 每当顾客往下翻一点, 店员就悄悄“换页”展示新几款奶茶, 实际上整本菜单从没被一次性拿上来。📖
🪄 小结口诀:
“假装有万条, 实际只渲二十条, 滚动即换页, 内存轻松跑~”
🌷第163页:虚拟列表优点与应用场景
| 优点 | 举例 |
|---|---|
| ⚡ 性能好 | 渲染更快、不卡顿 |
| 💾 占内存少 | 页面元素少,内存占用小 |
| 🧩 可扩展 | 滚动加载、懒加载场景通用 |
🍰 生活场景:
- 聊天列表(微信/QQ);
- 评论区(小红书/B站);
- 商品列表(淘宝、京东)。
看起来无穷无尽,其实页面里就几十个节点在循环复用。
🧠第164页:正则表达式(RegExp)——登场啦!
💡 定义:
正则表达式(Regular Expression)是一种用于匹配字符串的“规则语言”。
简单来说:
它就像一只“智能放大镜🔍”, 能帮你在文字中快速找到特定内容,比如邮箱、手机号、URL。
💻 示例 1:
const reg = /hello/;
console.log(reg.test('hello world')); // true
💬 说明:
| 语法 | 含义 |
|---|---|
/pattern/ | 正则表达式写法 |
test() | 检查字符串是否匹配 |
| 返回值 | 匹配到返回 true,否则 false |
🍵 奶茶店比喻:
你用放大镜在菜单上找“芋圆奶茶”这个词。 一旦找到,就说“匹配成功 ✅”。 如果整份菜单都没出现,就返回“false ❌”。
🍧第165页:正则表达式的常用规则
这一页是常见正则符号速查表👇
| 符号 | 含义 | 举例 |
|---|---|---|
. | 任意字符(除了换行) | /h.llo/ 匹配 hello, hallo |
^ | 以…开头 | /^a/ 匹配以 a 开头 |
$ | 以…结尾 | /b$/ 匹配以 b 结尾 |
\d | 数字(digit) | /\d/ 匹配 0–9 |
\w | 字母、数字、下划线 | /\w/ 匹配 a-z A-Z 0-9 _ |
\s | 空格 | /\s/ 匹配空白字符 |
+ | 至少一次 | /\d+/ 匹配 1234 |
* | 任意次(含0次) | /a*/ 匹配 aaaa、空 |
? | 0或1次 | /colou?r/ 匹配 color/colour |
💻 示例 2:
const reg = /^\d{3}-\d{8}$/;
console.log(reg.test('010-12345678')); // true
console.log(reg.test('12345678')); // false
🧠 解释:
^表示开头;\d{3}表示3位数字;-表示中间的横线;\d{8}表示8位数字;$表示结尾。
📞 匹配格式如“010-12345678”的电话号码。
🍰 奶茶店比喻:
正则就像一个“筛子”。 你把所有顾客手机号都倒进筛子, 只有格式正确的(比如“138开头的11位”)能被留下来, 其他乱填的“8888888888”会被过滤掉。😆
🎀 小可爱总结(第161~165页)
| 模块 | 核心知识 | 通俗解释 | 生活类比 |
|---|---|---|---|
| 虚拟列表 | 按需渲染 | 只显示看得到的部分 | 菜单翻页动态加载 |
| 滚动渲染 | 计算起始索引 + 替换渲染 | 滚动时换数据 | 店员不断“换菜单页” |
| 正则表达式 | 匹配规则语言 | 检查字符串是否符合格式 | 放大镜查特定奶茶 |
| 正则常用符号 | . ^ $ \d + * ? | 匹配字符、数量 | 菜单筛选规则 |
🌈 背口诀(小笨蛋版):
“虚拟列表防爆卡, 滚动渲染速度夸; 正则放大镜一拿, 手机邮箱都不怕~🔍✨”
🌸第166页:正则的“匹配方法”总览
JavaScript 中字符串和正则的“配合姿势”有好几种👇
| 方法 | 作用 | 返回值 |
|---|---|---|
test() | 测试字符串是否匹配 | ✅true / ❌false |
match() | 找到匹配项 | 匹配结果数组 |
matchAll() | 找到所有匹配项(更强) | 迭代器 |
search() | 返回匹配位置 | 匹配下标或 -1 |
replace() | 替换匹配项 | 替换后的字符串 |
split() | 按正则切割字符串 | 数组 |
exec() | 正则主动出击找内容 | 结果对象 |
🍵 奶茶店比喻:
想象你是奶茶店老板,有上千条顾客订单。 这些方法就像不同功能的“查询工具”:
| 工具 | 作用 |
|---|---|
test() | 看有没有人点“芋圆奶茶” |
match() | 找出第一个点“芋圆奶茶”的顾客 |
matchAll() | 找出所有点“芋圆奶茶”的顾客 |
replace() | 把“珍珠奶茶”全部改成“波霸奶茶” |
split() | 把一整条订单按逗号拆开 |
search() | 查这个关键词在第几个位置出现 |
exec() | 专业探员模式,一步步搜查每个匹配 |
🧋第167页:match() 用法详解
💻 代码:
let str = "I love JavaScript";
let reg = /love/;
let result = str.match(reg);
console.log(result[0]); // love
console.log(result.index); // 2
👉 match() 会返回一个包含匹配结果的数组。 第一个元素是匹配到的内容,index 表示它出现的位置。
🪄 举例类比:
顾客留言:“我超爱芋圆奶茶” 你用
match(/芋圆/)去查, 返回结果:“芋圆”,并告诉你出现在第 3 个字的位置~
💡 多个匹配项:
如果正则加了 g(全局匹配) 👇
let str = "apple banana apple";
let reg = /apple/g;
console.log(str.match(reg)); // ['apple', 'apple']
就会一次性找到所有的 apple~🍎🍎
🍰 奶茶店比喻:
“match” 就像服务员在菜单上帮你圈出“第一次”出现的芋圆奶茶; 加上
g后,服务员会把整份菜单上所有芋圆都圈出来 💪。
🌷第168页:matchAll()、search()、replace()
💻 matchAll() 找所有匹配项(含详细信息):
let str = "cat bat rat";
let reg = /[a-z]at/g;
for (let match of str.matchAll(reg)) {
console.log(match[0], "→ 位置", match.index);
}
输出:
cat → 位置 0
bat → 位置 4
rat → 位置 8
🧠 matchAll() = “带坐标的全面搜索”。
🍵 奶茶店:
老板查当天所有点“奶盖奶茶”的顾客, 还顺便记录他们在订单表里的行号 ✍️。
💻 search():返回第一个匹配位置
let str = "Hello JavaScript";
console.log(str.search(/Java/)); // 6
📍返回下标 6,表示“Java”从第 6 个字符开始。
🍰 类比:
你查“抹茶”在菜单第几页?search 就告诉你页码。
💻 replace():替换内容
let str = "I love JavaScript";
let newStr = str.replace(/JavaScript/, "TypeScript");
console.log(newStr); // I love TypeScript
🍵 类比:
菜单上“珍珠奶茶”卖完了?
replace()一键批量换成“波霸奶茶”~✨
💡 用正则替换多个:
let s = "cat bat rat";
console.log(s.replace(/[cr]at/g, "dog"));
// dog dog dog
[cr] 表示匹配 c 或 r。
☀️第169页:split() 与 exec()
💻 split():用正则分割字符串
let str = "apple,banana|orange";
let arr = str.split(/,||/);
console.log(arr); // ['apple', 'banana', 'orange']
🧠 /,||/ 表示“逗号 或 竖线”都能分割。
🍰 类比:
一整份订单写成:“珍珠奶茶,波霸奶茶|抹茶拿铁”,
split()帮你拆成三杯独立饮品。🥤
💻 exec():正则主动出击搜索内容
let str = "JavaScript is great";
let reg = /a./g;
let result;
while ((result = reg.exec(str))) {
console.log(result[0], "→ 位置", result.index);
}
输出:
av → 位置 1
as → 位置 3
🧠 exec() 每执行一次,就找下一个匹配项。
🍵 奶茶店:
“exec” 就像一个侦探, 每次搜索找到一杯奶茶后都会报告位置: “在第3排找到‘波霸’!在第5排找到‘抹茶’!”👮♀️
🌈第170页:test() 与 常见正则应用
💻 test() 检查是否匹配(最常用)
let reg = /\d+/;
console.log(reg.test("abc")); // false
console.log(reg.test("abc123")); // true
🧠 用途:
- 判断字符串中是否包含数字;
- 检查手机号格式;
- 验证邮箱地址。
🍰 奶茶店:
店长要过滤“手机号不完整”的顾客信息。 用
test()检查手机号格式, 合格的才进系统,不合格的打回重填 😆。
💻 应用示例(判断手机号):
let reg = /^1[3-9]\d{9}$/;
console.log(reg.test("13812345678")); // true
console.log(reg.test("123456")); // false
🧩 解释:
| 符号 | 含义 |
|---|---|
^ | 开头 |
1 | 必须以 1 开头 |
[3-9] | 第二位是 3~9 |
\d{9} | 后面9位数字 |
$ | 结尾 |
🍵 类比:
顾客登记手机号时,你设了一个筛子: “必须是 1 开头、11 位数、第二位3~9”。 正则 test() 一检查,不符合的直接弹个提示。📱
🎀 小可爱总结(第166~170页)
| 方法 | 功能 | 类比 |
|---|---|---|
test() | 测试是否匹配 | 看顾客手机号合不合格 |
match() | 找第一个匹配 | 圈出菜单里第一个“芋圆” |
matchAll() | 找全部匹配 | 查出所有“奶盖”并标位置 |
replace() | 替换匹配内容 | 把“珍珠”换成“波霸” |
split() | 按规则切割 | 把一长串订单拆成几杯 |
exec() | 循环匹配 | 侦探每次报告一个匹配 |
search() | 返回下标 | 告诉你关键词在哪一页 |
🌈 背口诀(小笨蛋专属记忆法):
“test试试看, match找第一个, matchAll找一串, replace来换换。 split来切切, exec细细捡, search报位置, 奶茶正则全不难~🧋✨”
🧋第171页:正则表达式实战(邮箱验证)
来看这段经典正则 👇
const emailReg = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$/;
console.log(emailReg.test('user@qq.com')); // true
console.log(emailReg.test('user@abc')); // false
🧠 解析:
| 片段 | 含义 | 举例说明 |
|---|---|---|
^ | 开头 | |
[a-zA-Z0-9_.-]+ | 用户名部分(字母、数字、下划线、点、横线) | user_01 |
@ | 固定的“@”符号 | |
[a-zA-Z0-9-]+ | 域名主体 | qq、gmail |
(.[a-zA-Z0-9-]+)* | 可以有多个子域名 | mail.qq.com |
.[a-zA-Z0-9]{2,6} | 顶级域名(2~6个字符) | .com / .cn |
$ | 结尾 |
🍰 生活比喻:
你在“奶茶店点单系统”里输入邮箱。 正则就像门口保安:
- 看看你邮箱是不是“先名字@品牌.后缀”;
- 如果写成“qq@com”或者“abc@”,直接挡在门外 🚫。
✅ 符合规则才让你下单成功~
🌸第172页:URL(网址)验证
这页展示了更复杂的正则,用来校验网址👇
const urlReg = /^(https?://)?([a-zA-Z0-9.-]+).([a-z]{2,6})([/\w.-]*)*/?$/;
console.log(urlReg.test("https://www.baidu.com")); // true
console.log(urlReg.test("ftp://example.com")); // false
💡 讲解结构:
| 片段 | 含义 | 举例 |
|---|---|---|
https?:// | http 或 https | 可选(?)代表s可有可无 |
[a-zA-Z0-9.-]+ | 域名部分 | www.baidu |
.[a-z]{2,6} | 顶级域名 | .com .net .cn |
([/\w.-]*)* | 路径 | /news/index.html |
/? | 末尾可有斜杠 | 允许结尾是 / |
🍵 奶茶店比喻:
顾客扫码点单时输入的网址要正确, 这个正则相当于“门口收银小姐姐”的智能识别系统:
- 看看是不是以 http 开头 ✅
- 有没有主域名(baidu) ✅
- 顶级域名合不合规(com、cn) ✅
- 路径写错(比如多空格)❌
🧠第173页:函数式编程是啥?
书里开始讲“函数式编程(Functional Programming)”了👇
📘 定义:
函数式编程是一种“用函数来描述计算过程”的编程思想。
✅ 数据是不可变的 ✅ 函数是纯函数(同输入 → 同输出) ✅ 函数可以组合起来使用
🍰 奶茶店比喻:
想象你开了一家“智能奶茶工厂”, 每个步骤(加茶、加奶、加糖)都是一个函数。
函数式编程就像把这些机器一条条接起来, 数据(原茶)进去,最后自动产出“完美奶茶”~
💻 代码例子:
const add = x => x + 10;
const multiply = x => x * 2;
const result = multiply(add(5));
console.log(result); // 30
🧠 解释: 1️⃣ 先执行 add(5) → 得到 15 2️⃣ 再执行 multiply(15) → 得到 30
🍵 “先加糖,再加倍”~
🌷第174页:函数式编程核心特征
| 特征 | 含义 | 奶茶店类比 |
|---|---|---|
| 🧊 纯函数(Pure Function) | 输入一样 → 输出一样,没有副作用 | “标准化奶茶机”,同配方出同味道 |
| 🧠 不可变性(Immutability) | 不直接修改原数据 | 不在原奶茶里乱加料,而是新配一杯 |
| ⚙️ 函数组合(Composition) | 函数嵌套调用、流水线式执行 | “加料→搅拌→封膜→出杯”流水线 |
| 🧩 高阶函数(Higher-order Function) | 函数当参数或返回值 | “自动配方机”,能返回新的机器功能 |
💻 示例:纯函数 vs 非纯函数
// 非纯函数(外部变量影响结果)
let sugar = 2;
function addSugar(milkTea) {
return milkTea + sugar;
}
// 纯函数(只看输入)
function addSugarPure(milkTea, sugar) {
return milkTea + sugar;
}
🍰 奶茶比喻:
“非纯函数”就像有个调皮的员工, 他会偷改全局变量的糖量 🧂,导致每杯味道不一样。
“纯函数”像自动奶茶机,每次按比例加糖,味道稳定~
☀️第175页:函数组合与管道(pipe)
书上展示了“函数嵌套”的升级写法👇
const compose = (f, g) => x => f(g(x));
const add = x => x + 2;
const square = x => x * x;
console.log(compose(square, add)(3)); // 25
💡 执行过程:
1️⃣ add(3) → 5 2️⃣ square(5) → 25 3️⃣ 最终输出 25
🍵 奶茶工厂比喻:
顾客点了一杯“加糖→再加倍奶”的奶茶。
- 第一台机器(add)加糖
- 第二台机器(square)加倍
compose()就是把两台机器串起来形成“流水线”生产。
📦 再看更高级一点:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add = x => x + 1;
const double = x => x * 2;
const minus = x => x - 3;
console.log(pipe(add, double, minus)(5)); // ((5+1)*2)-3 = 9
🧠 pipe() 就像是“左到右”的生产线; compose() 是“右到左”执行。
🍰 奶茶比喻:
pipe:先加糖 → 搅拌 → 封膜(从左到右) compose:封膜 ← 搅拌 ← 加糖(从右到左)
两种方式都能出同一杯完美奶茶~
🎀 小可爱总结(第171~175页)
| 模块 | 核心概念 | 通俗解释 | 类比 |
|---|---|---|---|
| 邮箱正则 | 检查邮箱格式 | 看输入是否像“名字@品牌.com” | 保安检查入店顾客 |
| URL 正则 | 检查网址格式 | 必须 http(s):// 开头 | 收银员识别扫码地址 |
| 函数式编程 | 用函数组合逻辑 | 把操作拆成小函数拼接执行 | 奶茶流水线生产 |
| 纯函数 | 无副作用 | 输入一样 → 输出一样 | 自动奶茶机 |
| 函数组合 compose/pipe | 函数嵌套执行 | “加糖→搅拌→封膜” | 多机协作 |
🌈 背口诀(记忆法):
“正则像保安, 邮箱URL都要查。
函数像流水线, 一步步造奶茶。
纯函数不乱动, 输出稳如家。🧋✨”
🌸第176页:函数柯里化(Currying)
💡 定义:
柯里化是把一个接受多个参数的函数, 变成一系列只接收一个参数的函数。
💻 示例:
function add(a, b) {
return a + b;
}
function curryAdd(a) {
return function (b) {
return a + b;
};
}
console.log(curryAdd(3)(5)); // 输出 8
🧠 解读:
- 普通函数
add(a, b)一次传两个参数。 - 柯里化后
curryAdd(3)返回一个新函数, 这个新函数再接收第二个参数5才计算。
🍰 奶茶店类比:
普通点单像这样: “请给我一杯奶茶+珍珠。”(一次下全单)
柯里化版是分步骤点单: “先来一杯奶茶☕。”(第1次) “再加珍珠🧋。”(第2次)
最后才组合出“珍珠奶茶”。
🌈 优点: 1️⃣ 提高函数复用率。 2️⃣ 可以分步传参,灵活应对不同场景。 3️⃣ 方便定制新功能。
🍵第177页:柯里化进阶应用
💻 高阶函数版:
const curry = fn => (...args) =>
args.length >= fn.length ? fn(...args) : (...next) => curry(fn)(...args, ...next);
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
🧠 解释:
fn.length是函数参数个数;- 每次传的参数没满时,返回新的函数继续接收;
- 当参数数量够了,就执行函数。
🍰 奶茶店比喻:
就像自动点单机:
- 第一次点“奶茶”;
- 第二次点“加珍珠”;
- 第三次点“加糖”;
全部选完才会真正下单出饮品。
✨ 记忆口诀:
“函数柯里化,拆单更优雅, 一步步加料,最后出好茶~🧋”
🌷第178页:惰性函数(Lazy Function)
💡 定义:
惰性函数指第一次调用后,会改变函数内部逻辑, 让后续调用更高效(避免重复判断)。
💻 示例:
function getBrowser() {
if ('ActiveXObject' in window) {
getBrowser = function () {
return 'IE';
};
} else {
getBrowser = function () {
return 'Not IE';
};
}
return getBrowser();
}
console.log(getBrowser()); // 第一次执行时判断
console.log(getBrowser()); // 第二次直接返回缓存结果
🧠 解释:
- 第一次调用时会判断浏览器类型;
- 执行完后“重写自己”,让后面直接走确定逻辑。
🍰 奶茶店比喻:
店员第一次问你:“要热的还是冰的?” 你回答“冰的”。 他记住了,下次再来就直接给你冰奶茶, 不再问第二次~😆
💎 优点:
- 减少重复判断;
- 提高性能;
- 逻辑更简洁。
☀️第179页:函数节流与防抖小回顾
这一页简短复习了:
- 节流 throttle → 一定时间内只触发一次;
- 防抖 debounce → 停止操作后才执行。
🍰 奶茶店比喻:
有顾客疯狂点“加糖加糖加糖!”
- 节流:每隔3秒才理一次他。
- 防抖:等他不再说话1秒后,再统一处理。
✨口诀:
“节流限频率,防抖等安静~”
🧠第180页:Web 攻击方式与防御(开篇)
从这页开始进入新章节——Web 安全!🧱 先讲 常见攻击方式:
- XSS(跨站脚本攻击)
- CSRF(跨站请求伪造)
- SQL 注入
📘 什么是 Web 攻击?
就是“坏人”通过浏览器输入恶意代码, 让系统执行本不该执行的内容。
🍵 举例:
- 输入框里不是输入名字,而是
<script>alert('你被黑啦')</script> - 系统没过滤,直接执行脚本 → 弹出框!💥
🧩 XSS 攻击(Cross-Site Scripting)
💻 示例:
<input id="name" />
<button onclick="sayHi()">提交</button>
<div id="output"></div>
<script>
function sayHi() {
const name = document.getElementById('name').value;
document.getElementById('output').innerHTML = `Hello ${name}`;
}
</script>
如果用户输入:
<script>alert("被攻击了!")</script>
页面会弹出警告框 ⚠️ 说明这段脚本被直接执行了。
🍰 奶茶店比喻:
顾客在点单机上,不老实地输入:“在杯子上写‘’”。
如果系统没检查,就真的把“脚本”写上去了…… → 店员(浏览器)被指挥去干坏事 💣。
💎 防御方法:
1️⃣ 对用户输入进行转义(HTML 编码)
function escapeHTML(str) {
return str.replace(/[<>&"]/g, t => ({
'<': '<',
'>': '>',
'&': '&',
'"': '"'
}[t]));
}
2️⃣ 不要用 innerHTML 直接插入内容 ✅ 改用 textContent 或 DOM API。
3️⃣ CSP(内容安全策略) 通过 HTTP 头部设置,只允许特定脚本执行。
🍵 奶茶店防御比喻:
- 在顾客点单机上装个“脏话过滤器”🧼
- 禁止输入
<script>、<img>等危险字。- 最后只允许“店长签名”的配方脚本被执行。
🎀 小可爱总结(第176~180页)
| 模块 | 概念 | 通俗解释 | 奶茶店比喻 |
|---|---|---|---|
| 柯里化 | 拆分多参数函数 | 分步点单 | 先点奶茶,再加料 |
| 惰性函数 | 第一次后自动优化 | 记住偏好 | 第一次问热冰,之后直接记 |
| 节流/防抖 | 控制触发频率 | 点单限速 | 节流=限频率,防抖=等安静 |
| XSS 攻击 | 恶意脚本执行 | 被顾客输入脚本坑 | 顾客乱写“偷密码”脚本 |
| XSS 防御 | 转义 + CSP | 输入过滤器 | 脏话检测机、店长签名脚本 |
🌈 背口诀:
“柯里拆单点,惰性记偏好; 节流控节奏,防抖等静好。
前端怕XSS,转义最重要; CSP来护法,网页更稳跑~🧋✨”
🌸第181页:XSS(跨站脚本攻击)进阶篇
在前面我们知道:XSS = 黑客把“恶意脚本”偷偷塞进网站。 现在这页讲三种主要类型👇
1️⃣ 存储型 XSS
💡 原理: 黑客把恶意代码存进数据库,其他人访问时都会被攻击。
🧩 示例:
论坛留言区中输入:
<script>alert('你被黑啦')</script>服务器把留言存数据库; 别人浏览这条留言时脚本被执行 💥。
🍰 奶茶店比喻:
有个调皮顾客在“留言墙”上写: “”。 店长把留言保存了, 后来每个顾客看留言时积分都被扣了 😱。
✅ 防御方法:
1️⃣ 用户输入 → 先转义(把 <、> 变成安全符号) 2️⃣ 输出内容时用 textContent 而不是 innerHTML 3️⃣ 开启 CSP 内容安全策略
2️⃣ 反射型 XSS
💡 原理: 恶意代码藏在 URL 里,服务器“反射”回网页执行。
🔍 示例:
https://example.com?name=<script>alert('XSS')</script>
当页面显示 Hello name 时,脚本被执行。
🍵 奶茶店比喻:
顾客点单时,在名字栏写
<script>偷积分</script>, 收银小哥傻傻地打印在小票上,系统执行了这段脚本 💣。
✅ 防御:
- 对 URL 参数进行转义;
- 严格过滤输入;
- 使用 HTTP Only Cookie 防止读取 Cookie。
3️⃣ DOM 型 XSS
💡 原理: 攻击发生在前端 JS 自己操作 DOM 时。 服务器没错,前端的 JS 把危险内容直接插进了页面。
🔍 示例:
let name = location.hash.slice(1);
document.body.innerHTML = name;
如果访问:
http://xxx.com#<script>alert('hack')</script>
也会触发!😨
🍰 奶茶店比喻:
顾客不是通过点单界面输入,而是直接改“链接参数”去偷偷控制店内机器。
✅ 防御:
- 避免
innerHTML; - 使用
DOMPurify这类库清理 HTML; - 做统一输入过滤。
☀️第182页:XSS 防御总结
| 类型 | 攻击入口 | 防御手段 |
|---|---|---|
| 存储型 | 数据库持久存储 | 输出转义 + CSP |
| 反射型 | URL 参数 | 输入过滤 + 转义 |
| DOM 型 | 前端操作 DOM | 禁止 innerHTML |
🍵 记忆口诀:
“存储留数据库,反射藏地址路, DOM前端乱输入,统一转义最靠谱~🧋”
🧩第183页:CSRF(跨站请求伪造)
💡 定义:
黑客“冒充你”,在你登录状态下,让你的浏览器偷偷发请求。
🍰 奶茶店比喻:
你登录了奶茶会员系统(浏览器里还带着登录 Cookie)。 结果有人发你个“优惠链接”:
https://奶茶店.com/deleteAccount😱 你一点击,它自动发请求把你的账号删掉了。
💻 示例攻击:
<img src="https://bank.com/transfer?to=hacker&amount=1000">
当用户访问这张图片时,浏览器自动带上 Cookie 去访问银行网站!
🧠 关键点:
- 浏览器会自动携带 Cookie;
- 用户本人没察觉;
- 攻击借助“信任的 Cookie”。
🌷第184页:CSRF 防御方法
| 防御方式 | 含义 |
|---|---|
| ✅ 验证 Referer | 检查请求来源是否合法 |
| ✅ 添加 Token | 每次请求加上随机校验码 |
| ✅ 使用 SameSite Cookie | 限制跨域 Cookie 发送 |
| ✅ 二次确认操作 | 敏感操作再输一次密码或验证码 |
🍵 奶茶店比喻:
| 手段 | 类比 |
|---|---|
| Referer 验证 | 检查点单页面是不是“官方小程序” |
| Token 验证 | 给每个订单贴唯一编号 |
| SameSite Cookie | 拒绝跨平台外卖系统偷偷点单 |
| 二次确认 | 下单前再让顾客输入一次会员号 |
💻 示例(添加 Token):
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="abc123">
<button>提交</button>
</form>
服务器会验证 token 是否匹配 ✅。
🍰 背口诀:
“CSRF偷你单, 验来源、加令牌, 同站Cookie守一关, 敏感操作双确认 🧋”
☀️第185页:SQL 注入(SQL Injection)
💉 原理:
黑客在输入框里注入 SQL 语句,让数据库执行恶意命令!
💻 示例:
SELECT * FROM users WHERE username = 'admin' AND password = '123456';
如果用户输入:
用户名:admin' --
拼接后的 SQL 就变成:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = '123456';
⚠️ -- 表示注释,后面内容都被忽略! 👉 黑客轻松登录后台!
🍰 奶茶店比喻:
顾客在“输入会员号”的地方写:
12345' 或 '1'='1系统以为是合法会员,放他进后台偷积分 💀。
✅ 防御方法:
| 防御手段 | 含义 |
|---|---|
| 参数化查询(Prepared Statement) | SQL 不拼字符串 |
| 输入过滤 | 禁止 ', ", --, ; 等 |
| 最小权限原则 | 数据库账号只给最少权限 |
| 日志监控 | 检测异常访问 |
💻 示例(参数化防御):
db.query('SELECT * FROM users WHERE username = ? AND password = ?', [user, pwd]);
这样用户输入就不会直接拼进 SQL 语句里。
🍵 奶茶店比喻:
防SQL注入 = 防止顾客在“输入会员号”里偷偷写脚本。
参数化查询就像:“只接收纯数字,不许混入奇怪符号!”😤
🎀 小可爱总结(第181~185页)
| 攻击类型 | 原理 | 奶茶店比喻 | 防御方法 |
|---|---|---|---|
| 存储型 XSS | 恶意脚本存数据库 | 留言墙写病毒 | 输出转义、CSP |
| 反射型 XSS | URL中藏脚本 | 链接带毒 | 输入过滤 |
| DOM型 XSS | 前端直接插入 | JS拼HTML被利用 | 不用innerHTML |
| CSRF | 伪造请求 | 外人冒充你下单 | Token、Referer、SameSite |
| SQL注入 | 注入恶意SQL | 输入会员号偷积分 | 参数化查询、过滤输入 |
🌈 背口诀(轻松记忆法):
“XSS偷前端,CSRF偷你单, SQL偷数据库,黑客真阴险!
转义加令牌,输入多过滤, 参数化最保险,奶茶店永安~🧋💪”
🍵第186页:什么是内存泄漏(Memory Leak)?
💡 定义:
内存泄漏 = 程序中某些数据用完了却没有被释放, 占着空间不走,让内存越用越多,最后卡死。
🍰 生活比喻:
想象你是奶茶店店长。 每天顾客用完的奶茶杯(内存)应该被回收。 但有个新来的实习生忘了扔垃圾,堆一堆杯子在桌上。
随着顾客越来越多,店里空间被“空杯子”占满 → 就是内存泄漏!
📈 图片含义(那张上升的蓝色图):
表示内存一直上涨、不掉下来。 如果程序正常,内存曲线应该是上升 → 回收 → 再上升 → 再回收。 一直上升说明内存泄漏。
🌸第187页:垃圾回收机制(Garbage Collection)
JavaScript 有自动回收机制 GC(Garbage Collector)♻️
💡 原理:
两种主要策略👇
| 策略 | 说明 | 类比 |
|---|---|---|
| 引用计数(Reference Counting) | 变量被引用 +1,引用断开 -1,计数为0时回收 | “每个奶茶杯都有使用次数标签” |
| 标记清除(Mark and Sweep) | 定期扫描可达对象,删除不再可访问的 | “每天清理掉无人认领的奶茶杯” |
💻 示例:引用计数
let a = { name: 'milk tea' };
let b = a;
a = null;
🧠 解释:
a最初指向{name: 'milk tea'},引用计数 = 2;a = null→ 减少1,但b还在引用;- 只有
b = null后,计数为 0 → 回收。
🍵 奶茶店比喻:
杯子(内存)被两个顾客共用; 一个走了没关系,另一个还没喝完; 只有当最后一个顾客也离开,杯子才被丢掉回收。
☀️第188页:闭包引起的内存泄漏
闭包(Closure)是 JS 的一大强大功能,但也可能藏垃圾 😅。
💻 示例:
function create() {
let name = "奶茶";
return function () {
console.log(name);
};
}
const drink = create();
drink 一直持有对 name 的引用,即使 create() 执行完也不会释放。
🍰 奶茶店比喻:
你雇了个实习生(闭包),他老是偷偷在心里记着“我那杯奶茶还没喝完”🍶。 结果这杯奶茶永远不会被丢掉 → 内存泄漏!
✅ 解决:
- 用完及时
drink = null; - 不要保留不必要的闭包变量;
- 监听大型对象是否被持续引用。
💻 示例2(DOM泄漏):
let el = document.getElementById('btn');
el.onclick = function() {
console.log(el.id);
}
这里函数引用了 el,即使元素被移除,也因为闭包而没被释放。
👉 解决:el.onclick = null;
🍵 奶茶店比喻:
你把“报废的点单机”拆下来了,但实习生还天天在想它的编号。
不清理掉记忆,就浪费空间~🧋
🌷第189页:常见内存泄漏场景总结
| 场景 | 原因 | 解决 |
|---|---|---|
| ✅ 意外的全局变量 | 忘写 let/const,变量挂到 window 上 | 使用严格模式 use strict |
| ✅ 定时器未清除 | setInterval 还在运行 | 手动 clearInterval |
| ✅ 闭包未释放 | 函数内引用未清理 | 用完设为 null |
| ✅ DOM 引用未解除 | 元素删了但引用还在 | 删除事件监听 |
| ✅ 缓存对象无限增长 | 数据不断 push 但不清理 | 控制缓存大小 |
🍰 奶茶店比喻:
| 场景 | 现实例子 |
|---|---|
| 全局变量 | 忘记关灯、所有人都能开电费💡 |
| 定时器 | 店员忘关“摇奶机” |
| 闭包 | 实习生记得太多没用的奶茶 |
| DOM 引用 | 拆掉旧点单机还没断电线 |
| 缓存 | 冰箱里老奶茶堆积不清理 |
✨ 记忆口诀:
“全局乱挂灯,定时机不关; 闭包藏奶茶,DOM断电难; 缓存堆成山,内存全占完 🧋💀。”
🧠第190页:JavaScript 的继承(Inheritance)
继承 = 让子类(新的对象)复用父类的属性和方法。 也就是“后代继承父母的技能”。
🍰 奶茶店比喻:
“珍珠奶茶”继承了“奶茶”的基本做法(加茶、加奶), 但又多了“加珍珠”特技 🧋。
💻 示例(原型继承):
function Parent() {
this.drink = 'milk tea';
}
Parent.prototype.sayHi = function() {
console.log('Hi, I love ' + this.drink);
}
function Child() {}
Child.prototype = new Parent();
const baby = new Child();
baby.sayHi(); // Hi, I love milk tea
🧠 解释:
Parent是父类;Child通过prototype继承;baby既有Parent的属性(drink),也能用方法(sayHi)。
🍵 奶茶店比喻:
“学徒奶茶师(Child)” 通过看师傅(Parent)学做奶茶, 所以他也能泡同样的奶茶,还能加自己的创意料。
✅ 常见继承方式:
| 方式 | 特点 |
|---|---|
| 原型链继承 | 共享父类属性(简单但会共用引用) |
| 构造函数继承 | 复制父类属性(不共用,但浪费内存) |
| 组合继承 | 两者结合,最常用 |
| ES6 Class 继承 | 更优雅的语法糖 |
💻 ES6 示例:
class Parent {
constructor() {
this.name = '奶茶';
}
sayHi() {
console.log('我是' + this.name);
}
}
class Child extends Parent {
constructor() {
super();
this.name = '珍珠奶茶';
}
}
const c = new Child();
c.sayHi(); // 我是珍珠奶茶
🍰 奶茶店比喻(强化版):
Parent是“标准奶茶配方”,Child是“珍珠奶茶新菜单”。
super()= 跑去父亲的厨房拿基本材料; 再加上自己的特色 → “珍珠奶茶出锅!”✨
🎀 小可爱总结(第186~190页)
| 模块 | 核心概念 | 类比记忆 |
|---|---|---|
| 内存泄漏 | 占着资源不释放 | 奶茶杯不扔掉 |
| 垃圾回收 | 自动清理机制 | 打扫阿姨每天清理桌面 |
| 引用计数 | 看引用次数 | 杯子还有人用就不丢 |
| 闭包泄漏 | 函数记太多 | 实习生藏奶茶不扔 |
| DOM 泄漏 | 删除节点没断线 | 拆掉点单机还插电 |
| 继承 | 子类复用父类方法 | 学徒继承师傅技能 |
| ES6 Class | 优雅继承写法 | 用模板做新品奶茶 |
✨ 背口诀:
“垃圾回收像阿姨,闭包奶茶藏心里, 定时器要记得停,DOM线断才干净。
继承就像学徒制,父传技艺加创意~🧋🎓”
🌸第191页:ES6 继承基础篇
💡 什么是继承?
继承 = “子类”获得“父类”的属性和方法。
换句话说, 学徒(子类)能用师傅(父类)教的技能,还能自己创新。
💻 示例一:普通继承
class Parent {
constructor() {
this.name = '奶茶师傅';
}
sayHi() {
console.log('我是' + this.name);
}
}
class Child extends Parent {
constructor() {
super(); // 调用父类构造函数
this.name = '学徒';
}
}
const c = new Child();
c.sayHi(); // 输出:我是学徒
🧠 解释:
extends表示“继承谁”;super()就是去父类的厨房拿基本材料;- 子类可以重写父类属性。
🍰 奶茶店比喻:
“奶茶师傅”教徒弟泡奶茶,徒弟先照着学(
super()), 但又想自己加点珍珠(改名叫“学徒”)。所以
extends就像“拜师”,super()就是“跟师学习基本技能”。
☀️第192页:ES6 继承细节
💡 super 的两种用法:
| 用法 | 说明 | 举例 |
|---|---|---|
| 在构造函数里 | 调用父类构造函数 | super() |
| 在普通方法里 | 调用父类方法 | super.sayHi() |
💻 示例二:方法调用
class Parent {
sayHi() {
console.log('奶茶师傅说:要认真泡茶!');
}
}
class Child extends Parent {
sayHi() {
super.sayHi();
console.log('学徒回:好嘞,师傅!');
}
}
new Child().sayHi();
✅ 输出:
奶茶师傅说:要认真泡茶!
学徒回:好嘞,师傅!
🍵 类比:
就像你对师傅说:“我会泡茶啦~” 但在开口前你先引用师傅的经典语录, 再加上自己的回答,谦虚又可爱😆。
🌷第193页:继承实现方式(古早 JS 时代)
在 ES6 出现之前,JS 有多种“继承写法”, 这些是面试常考题!🔥
🧱 四种常见继承:
| 类型 | 特点 |
|---|---|
| 1️⃣ 原型链继承 | 让子类原型指向父类实例 |
| 2️⃣ 构造函数继承 | 在子类中调用父类函数 |
| 3️⃣ 组合继承 | 两种结合(最常见) |
| 4️⃣ 寄生组合继承 | 最完美方案(性能好) |
💻 1️⃣ 原型链继承
function Parent() {
this.name = '奶茶师傅';
}
Parent.prototype.getName = function() {
console.log(this.name);
}
function Child() {}
Child.prototype = new Parent();
let c = new Child();
c.getName(); // 奶茶师傅
🍵 缺点:
如果父类里有引用类型属性(如数组),所有子类会共享。
比如:
function Parent() {
this.drinks = ['奶茶'];
}
let c1 = new Child();
let c2 = new Child();
c1.drinks.push('咖啡');
console.log(c2.drinks); // ['奶茶', '咖啡']
☠️ 子类之间串味儿了!
🍰 比喻:
所有徒弟共用一个奶茶桶, 一个徒弟偷偷加了咖啡,全店味儿都变了。☕💦
☀️第194页:构造函数继承
💻 示例:
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name); // 调用父构造函数
}
let c1 = new Child('珍珠奶茶');
let c2 = new Child('抹茶奶茶');
console.log(c1.name); // 珍珠奶茶
console.log(c2.name); // 抹茶奶茶
🧠 解释:
Parent.call(this)让子类拥有父类属性;- 各自独立,不共享;
- 但!父类原型上的方法没继承下来。
🍵 比喻:
每个学徒有自己独立的材料(不会串味), 但师傅没教他们泡茶的“秘诀”(方法继承不到)。
🌸第195页:组合继承(最常考)
组合继承 = 原型链 + 构造函数继承 → 兼顾方法复用 + 属性独立。
💻 示例:
function Parent(name) {
this.name = name;
this.drinks = ['奶茶'];
}
Parent.prototype.sayHi = function() {
console.log('Hi, 我是' + this.name);
}
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
Child.prototype = new Parent(); // 继承方法
Child.prototype.constructor = Child;
const c1 = new Child('珍珠奶茶', 2);
c1.drinks.push('芋圆');
c1.sayHi();
✅ 输出:
Hi, 我是珍珠奶茶
🧠 优点:
- 属性各自独立;
- 方法复用;
- 不会“串味”;
- 是早期最完美的继承方案 💎。
🍰 奶茶店比喻:
每个学徒(子类)自己有一桶材料(属性独立); 但都能来“看师傅的笔记”(共享原型方法)。
又独立又共享,完美培训体系~✨
💎 最终进化:寄生组合继承
ES6 的 extends 实际上就是寄生组合继承的语法糖。 它的核心思想是:
“别重复调用父类构造函数, 用一个中间空对象连接父子原型。”
🍵 比喻:
寄生组合继承 = 师傅留下一本“教学手册”(原型), 学徒拿到副本继续练, 不需要再跑去跟师傅做重复训练。
🎀 小可爱总结(第191~195页)
| 继承方式 | 核心思想 | 优缺点 | 奶茶店比喻 |
|---|---|---|---|
| 原型链继承 | 子类原型 = 父类实例 | 方法共享但属性共享(串味) | 多个徒弟共一桶奶茶 |
| 构造函数继承 | 父类.call(this) | 属性独立但方法不共享 | 每人独立材料但没学手艺 |
| 组合继承 | 属性独立 + 方法共享 | 稍冗余 | 有自己的桶又看师傅笔记 |
| 寄生组合继承 | 最优方案 | 高效、完美 | 师傅写手册,徒弟看副本 |
| ES6 class继承 | 语法糖版 | 最清爽 | 一键拜师系统! |
🌈 背口诀:
“原型共桶味,构造各自备; 组合双保险,寄生最完美。
ES6糖衣裹,拜师写更美~🧋✨”
🌸 第196页:寄生组合继承(终极继承方案)
前面几页我们已经知道:
- 原型链继承:方法能共享但属性会串味;
- 构造函数继承:属性独立但方法没继承;
- 组合继承:两者结合但会重复执行父类构造函数 💦。
所以! 👉 寄生组合继承 就是前端江湖中的 “终极拜师系统” 😎 ——既避免重复调用,又继承干净整洁。
💻 示例:
let parent = {
name: '师傅',
tools: ['勺子', '奶盖机'],
teach() {
console.log('教学泡奶茶技巧');
}
};
let child = Object.create(parent); // 重点!
child.name = '学徒';
child.tools.push('吸管');
child.teach();
✅ 输出:
教学泡奶茶技巧
🧠 解释:
Object.create(parent)创建一个以 parent 为原型的新对象;- 不会执行
parent的构造函数; - 子类对象既能访问父类的方法,又能独立拥有属性。
🍵 奶茶店比喻:
师傅(parent)写了一本“泡奶茶秘籍”; 学徒(child)复制了一本副本(Object.create), 自己笔记里可以改内容(属性独立), 但还能查阅师傅的原版秘籍(方法继承)。
✨ 核心一句话记:
✅
Object.create()= 创建一个带“父类基因”的新对象, 不执行父类构造,不重复“做奶茶”。
☀️ 第197页:寄生组合继承的完整实现
在传统函数写法中,我们常这样写👇
💻 示例:
function Parent(name) {
this.name = name;
this.tools = ['勺子', '奶盖机'];
}
Parent.prototype.teach = function() {
console.log('教学泡奶茶技巧');
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 继承方法
Child.prototype.constructor = Child; // 修正构造器
let c1 = new Child('珍珠奶茶', 2);
let c2 = new Child('抹茶奶茶', 3);
c1.tools.push('吸管');
console.log(c1.tools); // ['勺子','奶盖机','吸管']
console.log(c2.tools); // ['勺子','奶盖机']
🧠 重点说明:
Parent.call(this)→ 拿到属性(独立桶);Object.create(Parent.prototype)→ 拿到方法(共用秘籍);constructor = Child→ 修复继承链;- ✅ “既不串味,又不浪费”。
🍰 奶茶店比喻:
每个学徒各有奶茶桶(属性独立); 但大家共用一本《奶茶手册》(原型方法共享)。
师傅写一次就够,不需要每个徒弟再重做一遍 💪。
🌷 第198页:寄生组合继承图解(面试常画图)
那张图中,关键是理解 原型链关系。
🧩 结构如下:
Child.prototype → Parent.prototype → Object.prototype
- 最底层:
Object.prototype是所有对象的祖宗。 Child的原型链一路向上能找到Parent的方法。
🍵 奶茶店比喻:
👴 老祖宗:Object.prototype = 最初的“茶祖”。 👨🏫 师傅:Parent.prototype = 奶茶学校老师。 🧑🎓 学徒:Child.prototype = 学生,能继承老师的技艺。
所以当学徒调用 sayHi():
系统先在自己笔记里找(实例),找不到 → 去查师傅手册(Parent.prototype) → 再查茶祖宝典(Object.prototype)。
✅ 图形记忆口诀:
对象的属性查找路径 = “自己 → 父类原型 → Object原型”。
💡 第199页:ES6 class 实现寄生组合继承(语法糖)
ES6 其实把上面的过程全都自动化了!🤖
💻 示例:
class Parent {
constructor(name) {
this.name = name;
this.tools = ['茶壶', '奶盖机'];
}
teach() {
console.log(this.name + ' 教你泡奶茶');
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
const c = new Child('学徒小可爱', 18);
c.teach(); // 学徒小可爱 教你泡奶茶
🧠 解读:
extends自动建立原型链;super()自动调用父类构造;- 等价于我们手写的寄生组合继承。
🍰 奶茶店比喻:
ES6 帮你自动“拜师 + 建立传承档案 + 分桶装奶茶”~ 一行
extends就能完成整套继承仪式!✨
✅ 小结一句话:
ES6 class = 人类版语法糖 + 寄生组合继承自动机 🧋
🌈 第200页:JavaScript 浮点数精度问题(0.1 + 0.2 ≠ 0.3)
💡 问题:
0.1 + 0.2 === 0.3 // ❌ false
为什么?😱 因为 JavaScript 的数字是用 IEEE 754 双精度浮点数 存储的, 十进制转二进制时有误差!
🧠 比喻说明:
就像我们做奶茶时,要称 0.1 克糖粉。 但秤只能精确到“0.0001” 克, 结果每次称完都差一点点, 累积起来就不是 0.3 而是 0.30000000000000004。
📘 原因:
二进制中无法精确表示 0.1 或 0.2, 所以 JS 计算机算的其实是
0.10000000000000000555 + 0.2000000000000000111
💻 解决办法:
1️⃣ 使用 toFixed:
let res = (0.1 + 0.2).toFixed(2);
console.log(res); // "0.30"
⚠️ 注意:toFixed 返回的是字符串。
2️⃣ 使用 Number.EPSILON:
function equal(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(equal(0.1 + 0.2, 0.3)); // ✅ true
3️⃣ 放大再缩小:
let sum = (0.1 * 10 + 0.2 * 10) / 10;
console.log(sum); // 0.3
🍵 奶茶店比喻:
秤太精细不准怎么办? 👉 先称 10 勺糖(整数不会出错),再除以 10。
这就叫“放大再缩小”技巧!
🎀 小可爱总结(第196~200页)
| 知识点 | 概念 | 类比记忆 |
|---|---|---|
| Object.create | 只继承原型,不执行构造 | 学徒复制师傅手册 |
| 寄生组合继承 | 属性独立 + 方法共享 | 徒弟有自己桶又能看师傅笔记 |
| 原型链 | 对象属性查找路径 | 先找自己,再问师傅,再问茶祖 |
| ES6 class继承 | 寄生组合继承语法糖 | 一键拜师系统 |
| 浮点数精度问题 | 0.1+0.2≠0.3 | 奶茶称糖不准 |
| 精度解决 | toFixed / EPSILON / 放大再缩小 | 四舍五入 / 误差容忍 / 称多再除 |
🌈 背口诀:
“寄生组合最完美,class语法糖更美; Object.create建传承,原型链路有祖宗;
浮点误差似糖称,放大缩小巧调平~🧋✨”
🌸 第201页:JavaScript 的数字存储原理(IEEE 754)
JavaScript 里的所有数字(无论整数还是小数) 👉 都是用 64位二进制浮点数(IEEE 754标准) 存储的。
💡 结构分布:
| 部分 | 占用位数 | 含义 | 类比 |
|---|---|---|---|
| 符号位 sign | 1 位 | 表示正负号 | 奶茶是热的还是冷的 🌡️ |
| 指数位 exponent | 11 位 | 表示数量级(放大倍数) | 奶茶容量倍数:中杯、大杯、超大杯 ☕ |
| 尾数位 mantissa | 52 位 | 表示具体数字 | 奶茶里具体糖量、茶量 🍵 |
🍰 记忆口诀:
“一符号、十一指数、五十二尾巴” → 一杯奶茶(1 bit 热度)+ 一层杯套(11 bit 容量)+ 一堆奶茶料(52 bit 精度)
☀️ 第202页:二进制浮点数的坑(0.1 + 0.2 ≠ 0.3)
💻 代码:
console.log(0.1 + 0.2 === 0.3); // false
结果竟然是 false 😱! 为什么?因为计算机是用 二进制(0 和 1) 表示小数的。
💡 举例:
十进制的 0.1 = 二进制中无限循环小数:
0.1(10) = 0.0001100110011001100...(2)
计算机存储位数有限(只有52位尾数), 所以它只能截断存储。 → 误差就这样产生了。
🍵 奶茶店比喻:
就像电子秤称糖粉时,只能显示到小数点后两位。 你称 0.1 克糖时,其实是 0.100000000000000005 克。
三杯奶茶糖量累积误差 → 不等于理想值 0.3!
📈 图(那根红色二进制条)解释:
那张图展示了浮点数如何拆分:
- 左边 1 bit:正负号;
- 中间 11 bit:指数;
- 右边 52 bit:尾数(存小数部分)。 🧠 所以 JS 只能“近似表示”小数,不是真实值。
🌷 第203页:问题分析与打印验证
💻 示例:
console.log(0.1 + 0.2); // 0.30000000000000004
🧠 打印出来的数字多了个“尾巴” → 这是二进制转换回十进制时的误差。
💻 验证误差:
console.log(0.1.toString(2));
console.log(0.2.toString(2));
✅ 输出(都是无限循环):
0.0001100110011001100110011001100110011...
0.0011001100110011001100110011001100110...
🍰 奶茶店类比:
想象糖粉机每次掉粉的量是固定的二进制单位(比如一粒=0.0001g)。 但想称“0.1克”时,机器掉不出精确的 0.1g, 所以只能凑个差不多的值——“0.100000000000000005g”。
✅ 结论:
二进制表示十进制小数时,某些数字永远无法精确表示。 (这不是 JS 的锅,是计算机通病 🖥️)
💫 第204页:解决方案总结
💻 方法1️⃣:toFixed 保留小数位
let sum = (0.1 + 0.2).toFixed(2);
console.log(sum); // "0.30"
⚠️ 注意:
- 返回的是字符串;
- 想当成数字用得转回:
Number(sum)。
💻 方法2️⃣:放大再缩小
let sum = (0.1 * 10 + 0.2 * 10) / 10;
console.log(sum); // 0.3
📦 逻辑:
先“整数化”再算,最后除回去。 因为整数在二进制里是精准的。
🍵 奶茶店比喻:
称糖太精细会误差,那就一次称 10 勺糖(整数不会错), 分成 10 杯奶茶。 这样每杯都能精确到刚好 0.1g ✨。
💻 方法3️⃣:用误差容忍度比较(EPSILON)
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
✅ EPSILON 表示 JS 能接受的最小误差值(≈ 2.22e-16)
🍰 类比:
“只要误差小到人尝不出来,就算一杯完美的奶茶!”😋
💎 方法4️⃣:使用高精度库(Big.js / Decimal.js)
如果要做金融、计费、科学计算 → 必须用这些库。
它们底层会用字符串或大整数存储,不丢精度。
🧠 第205页:尾递归(Tail Recursion)
终于到“算法小魔王”登场啦!
💡 什么是递归?
递归(Recursion) = 函数自己调用自己。
比如👇👇👇
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1);
}
console.log(sum(3)); // 6
执行过程:
sum(3)
= 3 + sum(2)
= 3 + 2 + sum(1)
= 3 + 2 + 1
🍵 奶茶店比喻:
老板要统计一天卖了几杯奶茶。 他叫你帮忙统计第 N 杯; 你又让学徒统计 N-1 杯; 学徒再让实习生统计 N-2 杯…… 一层一层往下传。
⚠️ 问题: 每一层都要“记住上一层”, → 就像堆积太多任务单,最终“爆栈”☠️。
💡 尾递归(Tail Recursion)登场!
尾递归就是一种优化写法, 👉 让每次递归不再堆栈,只保留当前一步。
💻 示例:
普通递归:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
尾递归优化版:
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
console.log(factorial(5)); // 120
🧠 解释:
- 普通递归:每次“等结果”→ 堆满栈。
- 尾递归:直接把中间结果传下去,像“接力棒”一样传递。
🍰 奶茶店比喻:
你不用每次都等前一位学徒报数, 而是让每个人直接在点单单上写下总数接着传下去。
这样永远只用一张单子,店里也不会爆单啦!💨
✅ 尾递归特点:
- 调用在函数最后一步;
- 无需保存上层环境;
- 不会造成栈溢出;
- 现代 JS 引擎(部分)支持优化。
🎀 小可爱总结(第201~205页)
| 知识点 | 核心思想 | 奶茶店比喻 |
|---|---|---|
| IEEE 754 | JS 数字 = 浮点数结构 | 一符号、十一指数、五十二尾巴 |
| 精度误差 | 0.1 + 0.2 ≠ 0.3 | 秤太精细糖粉凑不准 |
| toFixed | 保留小数位 | 人工修整 |
| 放大再缩小 | 乘 10 再除 10 | 先称整勺再分 |
| EPSILON | 误差容忍区间 | 味觉允许范围 |
| 尾递归 | 优化递归防爆栈 | 点单接力传递法 |
🌈 背口诀:
“浮点精度糖称偏,放大缩小误差免; toFixed整修味更甜,EPSILON忍误差一点。
尾递归传接力棒,栈不爆炸奶茶香~🧋✨”
🌸 第206页:递归的原理(函数自己调自己)
💡 什么是递归?
递归就是:函数在执行过程中自己调用自己。
举个例子: “老板问:第10杯奶茶的销量是多少?” 员工回答:“我先去问第9杯的销量,再加上第10杯。”
于是,函数一直往下问,直到问到第1杯为止~
💻 示例:阶乘(factorial)
function pow(x, n) {
if (n == 1) return x; // 递归出口
else return x * pow(x, n - 1);
}
console.log(pow(2, 3)); // 8
🧠 执行过程拆解:
pow(2, 3)
= 2 * pow(2, 2)
= 2 * (2 * pow(2, 1))
= 2 * (2 * 2)
= 8
📊 图中那张「递归流程图」解释:
- 判断
n == 1?是 → 返回;否则 → 再次递归调用; - 每一层都要等待下一层完成,像一个「递归栈」🪜。
🍵 奶茶店比喻:
“老板问第10杯销量”, 你去问店员A(第9杯), 店员A问店员B(第8杯), 一层一层传下去…… 最后第1杯报上来,再逐层相加。
缺点:所有人都要记住前面问的是什么。
⚠️ 这就像堆栈太多订单,店员容易脑袋炸💥!
☀️ 第207页:循环与递归的对比
递归其实可以被“循环”替代。
💻 示例:循环版的阶乘
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
console.log(pow(2, 3)); // 8
✅ 结果一样,但循环更节省内存。
🍰 类比:
循环像是在流水线奶茶机上操作,每次搅拌一次、往前推进。 而递归像是人工传递杯子,一层层往下传、再逐层往上回报。
所以:
- 循环:高效;
- 递归:逻辑清晰;
- 尾递归:两者结合,既清晰又高效💪。
🌷 第208页:尾递归(Tail Recursion)强化理解
💡 复习:
尾递归就是——“递归调用在函数的最后一步执行”, 且不依赖上一次结果。
💻 示例对比:
普通递归:
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1);
}
尾递归优化:
function sum(n, total = 0) {
if (n === 1) return total + 1;
return sum(n - 1, total + n);
}
console.log(sum(5)); // 15
🧠 执行逻辑差异:
| 普通递归 | 尾递归 |
|---|---|
| 每次都等下一层返回结果 | 每次直接把结果往下传 |
| 占用多层栈内存 | 只用一层(优化) |
🍵 奶茶店比喻:
普通递归:每个员工都要记住“上一杯卖多少”。 尾递归:只用一张“累加订单表”传下去,每个人只改数字,不留旧单。
✅ 节省空间,不会爆栈!
💫 第209页:递归实战——树形结构遍历
现在进入递归最常见的面试题应用!
💡 树形结构(Tree)
比如菜单栏、评论回复、组织架构、部门层级…… 都是「一层套一层」的树状结构。
💻 示例:
let tree = {
name: '店长',
children: [
{
name: '收银员',
children: [
{ name: '点单A' },
{ name: '点单B' }
]
},
{
name: '制茶师',
children: [
{ name: '调茶A' },
{ name: '调茶B' }
]
}
]
};
function traverse(node) {
console.log(node.name);
if (node.children) {
node.children.forEach(child => traverse(child));
}
}
traverse(tree);
✅ 输出:
店长
收银员
点单A
点单B
制茶师
调茶A
调茶B
🍰 奶茶店比喻:
递归遍历树形结构就像逐层点名考勤~
你(店长)先喊“收银员、制茶师”; 再让他们分别喊出自己的员工; 直到最底层。
🧠 思维口诀:
“自己干完,再交给孩子干;孩子干完,再交给孙子干。”
🌈 第210页:递归的应用与总结
💡 实际应用场景:
| 场景 | 说明 |
|---|---|
| DOM 遍历 | 遍历网页节点树 |
| 文件目录读取 | 一层层读取子文件夹 |
| 数据结构 | 树形菜单、评论嵌套 |
| 数学算法 | 阶乘、斐波那契数列 |
| Vue/React | 虚拟DOM Diff算法 |
💻 小结对比:
| 类型 | 特点 | 缺点 | 适用场景 |
|---|---|---|---|
| 普通递归 | 简单直观 | 容易爆栈 | 小规模数据 |
| 尾递归 | 空间高效 | 兼容性一般 | 大规模计算 |
| 循环 | 性能好 | 逻辑复杂 | 数值运算类 |
🍵 奶茶店记忆法:
| 递归类型 | 奶茶店比喻 |
|---|---|
| 普通递归 | 员工逐层传话,最后汇总 |
| 尾递归 | 一张累加表传到底,不爆栈 |
| 循环 | 自动流水线奶茶机 |
🎀 小可爱总结(第206~210页)
| 知识点 | 核心理解 | 奶茶店例子 |
|---|---|---|
| 递归 | 函数自己调自己 | 店员层层传话 |
| 尾递归 | 结果往下传 | 传累加表 |
| 循环 | 不用栈 | 奶茶机自动化 |
| 树形递归 | 逐层遍历结构 | 点名考勤制茶团队 |
🌈 背口诀:
“递归层层问到底,尾递接力不爆栈; 树形菜单靠遍历,循环流水效率赞~🧋✨”