20251018-JavaScript八股文整理版(下篇)

121 阅读47分钟

🧋第 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():只要有一个符合条件就返回 true
  • every():所有都要符合才 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 页)

(图片上那张框图说明的逻辑是 👇)

image.png

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️⃣ AD 是主线程同步任务。 2️⃣ Promise.then 是微任务,会在本轮结束后执行。 3️⃣ setTimeout 是宏任务,要等下一轮事件循环。

🍰 类比:

你下单时(A、D)先立刻喊出来; 奶茶师加珍珠(C)马上做完; 下一个顾客的单(B)等前面全处理完再来。


☀️第 114 页:宏任务 vs 微任务 流程图

图片上那张流程图就是 👇 的意思:

┌────────────────┐
│ 执行主线程代码 │ ← A D
└─────┬──────────┘
      │ 执行完后
      ▼
┌────────────────┐
│ 执行所有微任务 │ ← C
└─────┬──────────┘
      │ 全部完成后
      ▼
┌────────────────┐
│ 取下一个宏任务 │ ← B
└────────────────┘

也就是:

一轮循环顺序:主线程 → 微任务 → 下一个宏任务


🧋第 115 页:更细节的执行顺序

这页主要帮你确认细节:

✅ 常见宏任务:

  • 整个 script 文件
  • setTimeout
  • setInterval
  • I/O
  • UI 渲染

✅ 常见微任务:

  • Promise.then
  • MutationObserver
  • queueMicrotask

🍵 类比总结:

一次循环(做奶茶)流程是: 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️⃣ 15 → 同步任务 2️⃣ 3 → 微任务(Promise.then) 3️⃣ 24 → 宏任务(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(会话存储)

💡 概念

sessionStoragelocalStorage 很像, 区别在于:它只在当前标签页生效

🍰 类比:

就像你在奶茶店点了一杯饮品, 但只在这一桌喝完作废。 一旦你离开(关闭标签页), 订单记录就消失。


示例:

// 保存
sessionStorage.setItem('temp', '红豆奶茶');
​
// 获取
console.log(sessionStorage.getItem('temp')); // 红豆奶茶// 删除
sessionStorage.removeItem('temp');

🧊 关闭浏览器或标签页后再打开,sessionStorage 数据会自动消失。


✅ 使用场景

场景使用类型说明
保存登录状态(短期)sessionStorage关掉页面即清除
用户偏好(长期保存)localStorage页面刷新也保留
Cookie 登录信息cookie会发给服务器验证

💡 小可爱口诀:

“Session 暂寄桌,Local 永存柜, Cookie 送上菜,一锅三味配!”


☀️第 123 页:存储方式的比较总结表

项目cookielocalStoragesessionStorage
存储大小~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)把文件从 startend 截一段(像切蛋糕)
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;
}

image.png

💬 一步步讲:

名称意思
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 + scrollTop
  • getBoundingClientRect() 来判断一个元素是不是在视口里。

现在浏览器给我们出了个新神器: 🎯 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.comb.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跳转回原页面并携带 tokenwindow.location.href
5子系统保存 tokenlocalStorage.setItem
6每次请求带 tokenAuthorization 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同一家族域名可自动登录奶茶总部发的会员卡
跨域SSOtoken机制 + 登录中心不同品牌共享登录中心奶茶联盟总部发会员号
上拉加载监听滚动到页面底部滚到底再加载新内容菜单滑到最后自动加新品
下拉刷新监听滑到页面顶部往下拉触发刷新菜单往下拉刷新出新品

🌈 背口诀(超可爱版):

“同域靠卡、跨域靠号, 滚到底部再上货~ 向下拉拉新奶茶, 一次登录全通畅~🧋”

🧋第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 条;
  • 页面看起来就像“无限下滑”的奶茶菜单~🥤

🍰 奶茶店比喻:

顾客滑着菜单看完“奶茶13号”, 滑到底部时,店员悄悄塞上“奶茶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️⃣ 节流与防抖

  • 防止滚动事件频繁触发;
  • 可用 debouncethrottle 包裹 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 => ({
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
    '"': '&quot;'
  }[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
反射型 XSSURL中藏脚本链接带毒输入过滤
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.prototypeParent.prototypeObject.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标准) 存储的。


💡 结构分布:

部分占用位数含义类比
符号位 sign1 位表示正负号奶茶是热的还是冷的 🌡️
指数位 exponent11 位表示数量级(放大倍数)奶茶容量倍数:中杯、大杯、超大杯 ☕
尾数位 mantissa52 位表示具体数字奶茶里具体糖量、茶量 🍵

🍰 记忆口诀:

“一符号、十一指数、五十二尾巴” → 一杯奶茶(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 754JS 数字 = 浮点数结构一符号、十一指数、五十二尾巴
精度误差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页)

知识点核心理解奶茶店例子
递归函数自己调自己店员层层传话
尾递归结果往下传传累加表
循环不用栈奶茶机自动化
树形递归逐层遍历结构点名考勤制茶团队

🌈 背口诀:

“递归层层问到底,尾递接力不爆栈; 树形菜单靠遍历,循环流水效率赞~🧋✨”