🌸 第 1 页:JS 数据类型总览
🧠 一句话记忆:
JS 的数据类型分两大类: 基本类型(值类型) vs 引用类型(对象类型)
分类 | 子类型 | 存储位置 |
---|---|---|
基本类型 | Number, String, Boolean, Null, Undefined, Symbol, BigInt | 栈内存(Stack) |
引用类型 | Object, Array, Function | 堆内存(Heap) |
📦 生活类比: 想象你的电脑桌上放两个地方:
- "📏小抽屉"(栈内存)放简单小物件(值类型)
- "📦大纸箱"(堆内存)放复杂的大玩意儿(对象)
小抽屉存的是实际的数值; 大纸箱存的是"标签"(引用),告诉你东西在哪个角落。
🌸 第 2 页:Number 类型
🧮 概念:
Number 表示数字,可以是整数、小数、正无穷 Infinity
、负无穷 -Infinity
、或者不是数的 NaN
(Not a Number)。
💻 示例代码:
ini
let a = 123;
let b = 1.23;
let c = Infinity;
let d = NaN;
💡 小笨蛋专属记忆法:
想象你在奶茶店当收银员:
- 123 元:正常顾客付款;
- 1.23 元:客人只买一小杯;
- Infinity:有人要"无穷续杯",你无语;
- NaN:客人拿树叶付款,系统:???
🌸 第 3 页:Undefined、Null、String
🌀 Undefined:
表示"变量声明了但没赋值"。
javascript
let a;
console.log(a); // undefined
🍵 类比:你有一个空奶茶杯子,但里面还没倒奶茶。
🚫 Null:
表示"空对象,没有内容"。
ini
let cup = null;
console.log(cup); // null
🍶 类比:杯子倒了奶茶但又被喝光了,里面是空的(杯子存在但内容没了)。
✨ String:
表示文本字符串,用
' '
或" "
或 `` 包起来。
ini
let name = '小可爱';
let desc = `今天学习 JavaScript`;
🍰 类比:你在奶茶杯上贴上标签写 "草莓味",这个标签就是字符串。
🌸 第 4 页:Boolean、Symbol
✅ Boolean:
只有两种取值:
true
或false
ini
let isCute = true;
let isTall = false;
🐰 类比:像开关一样,开灯/关灯,真/假。
🧩 Symbol:
表示唯一值,主要用于区分对象属性,防止命名冲突。
ini
let id1 = Symbol('id');
let id2 = Symbol('id');
console.log(id1 === id2); // false
🎀 类比:你和别人都叫"小可爱",但身份证号不同------Symbol 就是那个身份证号。
🌸 第 5 页:引用类型(Object, Array, Function)
🧱 Object(对象):
一组键值对(key-value)
ini
let person = {
name: '小可爱',
age: 18
};
🧋 类比:就像点奶茶的单子,写着「名字:小可爱」「口味:草莓味」。
🧮 Array(数组):
有序的值列表,用
[]
包住。
ini
let drinks = ['奶茶', '果汁', '可乐'];
🍹 类比:一排奶茶杯排队站好,编号从 0 开始。
🧠 Function(函数):
封装可复用的逻辑代码。
javascript
function sayHi(name) {
console.log('Hi ' + name);
}
sayHi('小可爱');
🎤 类比:这是一个"喊人函数",输入名字,它就帮你喊出来。
🍬 总结口诀
"七值三引记心间,栈装小值堆装函。 数字布尔加字符串,空空未定各分担。 对象数组函数连,Symbol 唯一独自仙。"
🌼 第 6 页:函数与存储区别(值类型 vs 引用类型)
🧩 1.3.4 其他引用类型
代码示例:
bash
function sum(a, b) {
return a + b;
}
let result = sum(1, 2);
💬 解释: 这里定义了一个函数 sum
,它像一个"小计算员"一样,你给他两个数 a
和 b
,他会算出和并告诉你结果。
📦 函数也是一种"引用类型",存在堆内存里。 👉 想象它是一个"奶茶机",机器说明书(函数体)放在堆里,调用时(执行)才会真的"打奶茶"。
🌸 第 7 页:存储区别
JavaScript 变量在底层的存储有两种:
- 基本类型存在"栈内存"(stack)
- 引用类型存在"堆内存"(heap)
🧠 1.4.1 基本类型
ini
let a = 10;
let b = a;
b = 20;
console.log(a, b); // 10, 20
📖 解释:
- 变量
a
存了一个数值10
; b = a
其实是复制了一份数值;- 改
b
不影响a
。
🍬 类比:
像你复制了一杯奶茶配方卡,自己改成多糖版,原来的不变。
🧱 1.4.2 引用类型
ini
let obj1 = { name: '小可爱' };
let obj2 = obj1;
obj2.name = '小笨蛋';
console.log(obj1.name); // 小笨蛋
💬 解释:
obj1
是个对象,存在堆中;obj2 = obj1
其实复制的是"地址标签";- 改
obj2
的内容,其实两人共用一杯奶茶!💥
🍵 类比:
你和朋友共用一杯奶茶(对象),谁加糖,整杯都变甜。
🌸 第 8 页:堆栈结构图
这页有一张大图,展示了:
- 栈内存(stack):存放变量名与引用地址;
- 堆内存(heap):存放对象真实数据;
- 箭头代表"引用关系"。
🧋 生活类比:
栈像你桌上的"备忘贴",写着「obj1 👉 柜子3号」; 堆像"奶茶仓库",柜子3号里放着真正的奶茶。 当你复制 obj2 = obj1,就是在贴一张新的备忘贴,但还是指向同一个柜子。
🌼 第 9 页:小结
重点回顾:
-
基本类型(栈内存):
- 存值;
- 拷贝的是值;
- 互不影响。
-
引用类型(堆内存):
- 存地址;
- 拷贝的是引用;
- 改一个,全体变化。
📦 口诀记忆:
"栈装值,堆装址,拷贝值不动,拷贝址同命。"
🌷 第 10 页:数据结构入门
🧭 2.1 什么是数据结构?
就是"存放数据的方式"。
举例:
- 书架的摆法不同(分类 vs 随意堆);
- 程序中的"数组、栈、队列"也各自有存放规则。
📚 2.2 数组(Array)
ini
let arr = [1, 2, 3];
📦 特点:
- 有序;
- 可通过索引(编号)访问。 🍵 类比:像奶茶杯一排排放好,每个都有编号。
🧱 2.3 栈(Stack)
一种 后进先出(LIFO) 的数据结构。
📘 图中: 栈顶进 → 栈顶出。
🧋 类比:
奶茶杯子堆叠起来,你最后放上去的那杯(最上面的),会最先被取走。
arduino
let stack = [];
stack.push('奶茶1');
stack.push('奶茶2');
stack.pop(); // 拿走奶茶2
🪜 2.4 队列(Queue)
一种 先进先出(FIFO) 的结构。
📘 图中: 队首出 → 队尾进。
🍹 类比:
排队买奶茶,先来的顾客先拿走,后来的慢慢排。
arduino
let queue = [];
queue.push('客人A');
queue.push('客人B');
queue.shift(); // 客人A先走
🎯 超萌总结(第6~10页)
概念 | 记忆口诀 | 生活类比 |
---|---|---|
基本类型 | 拷贝值,不连心 | 两张奶茶配方卡 |
引用类型 | 拷贝址,共命运 | 共喝一杯奶茶 |
栈 | 后进先出 | 奶茶杯堆叠 |
队列 | 先进先出 | 顾客排队买奶茶 |
🍓 第 11 页:队列结构延伸(Queue ➜ Linked List)
🪜 队列回顾
图上显示「先进先出 (FIFO)」队列流程: 排队买奶茶 → 先来的顾客先拿走 → 新顾客从队尾入队。
🧵 2.5 链表(Linked List)
链表是一种"节点(Node)串起来"的结构。 每个节点保存:
- 当前的数据;
- 下一个节点的引用(next 指针)。
💻 示例理解:
ini
let node1 = { value: 1, next: null };
let node2 = { value: 2, next: null };
node1.next = node2;
💬 解释:
- node1 里放了数字 1;
- 它的
next
指向 node2; - 这样就变成 "1 → 2" 的链式结构。
🍬 类比: 想象一串奶茶杯,每个杯子底部绑着一根绳子,绳子另一头连着下一个杯子。 要拿第 3 杯奶茶,得先摸第 1 杯 → 找到第 2 杯 → 再摸到第 3 杯。
🏷️ 2.6 字典(Map / Object)
字典是一种 "键值对存储" 结构。
bash
let dict = {
name: '小可爱',
drink: '草莓奶茶'
};
📘 解释:
- "name" 是 key;
- "小可爱" 是 value。
🍵 类比: 就像点单系统的"清单表":
名称 | 奶茶类型 |
---|---|
小可爱 | 草莓奶茶 |
你通过"名字(key)"找到对应的"奶茶(value)"。
🍪 2.7 集合(Set)
Set 是一种"不重复的值集合"。
sql
let drinks = new Set(['奶茶', '果汁', '奶茶']);
console.log(drinks); // Set(2) { '奶茶', '果汁' }
💬 解释: Set 自动去重!同样的奶茶只保留一杯。
🍰 类比:
你在收银台点单系统里重复点"奶茶",系统会说:"重复啦,不用再加一杯!"
🌸 第 12 页:DOM 操作
🏗️ 3. DOM 是什么?
DOM(Document Object Model)是网页的树状结构 。 每个标签(如 <div>
、<p>
、<img>
)都对应一个节点。
css
<div>
<p>Hello 小可爱</p>
</div>
📖 这其实是:
makefile
父节点: div
子节点: p
文本: Hello 小可爱
🍓 类比: 网页就像一棵"奶茶店组织树":
- 店长(div) ↳ 店员(p) ↳ 负责说"欢迎光临!"(文本内容)
🪄 第 13 页:DOM 操作常见方法
3.2 操作节点四大类:
- 创建(Create)
- 查找(Select)
- 修改(Update)
- 删除(Delete)
是不是很像奶茶店 CRUD 流程:
- Create:开新奶茶;
- Read:查看奶茶;
- Update:改口味;
- Delete:下架。
🧋 第 14 页:创建节点
🧱 3.2.1 创建节点
ini
let p = document.createElement('p'); // 创建 <p>
p.innerText = '草莓奶茶';
document.body.appendChild(p);
📖 解释:
createElement
------ 造一个新标签;innerText
------ 填入文字;appendChild
------ 把奶茶(元素)放到货架(页面)上。
🍹 类比:
你新做了一杯奶茶(创建节点), 在杯上写上口味(设置内容), 最后把它放到展示台(插入网页)。
✨ 第 15 页:修改、删除节点
🧾 修改属性
ini
let img = document.querySelector('img');
img.setAttribute('src', 'milk-tea.jpg');
→ 改图片地址(换奶茶照片 📸)
🚮 删除节点
ini
let p = document.querySelector('p');
p.remove(); // 删除 <p>
→ 把某段介绍(旧宣传语)删掉。
🌷 小结口诀(第 11~15 页)
概念 | 一句话记忆 | 生活类比 |
---|---|---|
链表 | 节点串成链 | 奶茶杯用绳子连起来 |
字典 | 键值对查找 | 通过顾客名字查点单 |
集合 | 去重集合 | 重复点的奶茶只保留一杯 |
DOM | 网页结构树 | 奶茶店组织架构图 |
CRUD | 增删查改 | 奶茶上新、下架、改口味 |
🎯 一句总结送给你:
"链表串串连,字典查得全; 集合不重样,DOM搭舞台。"
🧋 第 16 页:DOM 的层级与节点关系
💡 DOM 节点层次图
图中展示了 "父节点(Parent)---子节点(Child)---兄弟节点(Sibling)" 的关系。
🍵 类比成奶茶店:
-
店长(父节点)
- 店员 A(子节点)
- 店员 B(子节点)
- 店员 A 和 B 是"兄弟节点"
🧱 获取父子关系的代码
ini
let box = document.querySelector('.box');
let parent = box.parentNode; // 父节点
let first = box.firstChild; // 第一个子节点
let last = box.lastChild; // 最后一个子节点
let next = box.nextSibling; // 下一个兄弟节点
📖 解释:
parentNode
:找上级;firstChild
:找第一个孩子;lastChild
:找最后一个孩子;nextSibling
:找下一个同级。
🍬 类比:
就像你在员工名单上查"谁是我的上司 / 第一个同事 / 下一个排班同事"。
🌷 第 17 页:修改节点内容
3.2.2 修改节点内容
JS 中修改标签里的内容常见 3 种写法:
ini
div.innerText = '草莓奶茶';
div.textContent = '波霸奶茶';
div.innerHTML = '<p>新品:🍈哈密瓜奶绿</p>';
📖 区别:
innerText
:只改文字;textContent
:更快,也改文字;innerHTML
:能直接插入 HTML 标签。
🍹 类比:
innerText:换菜单文字 textContent:换更快的菜单 innerHTML:整张新菜单贴上去(含图文排版)
🌸 第 18 页:插入节点 / 移除节点
🧩 插入节点
ini
let p = document.createElement('p');
p.innerText = '新品上线啦!';
document.body.appendChild(p);
💬 就像你新做一杯奶茶 ➜ 放上货架展示。
🧺 插入指定位置
ini
let parent = document.querySelector('.menu');
let item = document.createElement('li');
item.innerText = '抹茶拿铁';
parent.insertBefore(item, parent.firstChild);
📘 解释: insertBefore
表示插在某个节点前面。 🍵 类比:
你要把新品"抹茶拿铁"插到菜单第一个位置。
🧹 删除节点
ini
let node = document.querySelector('p');
node.remove();
🍋 类比:
不好卖的奶茶,直接下架扔掉。
🍑 第 19 页:节点属性与样式
🧾 获取属性
ini
let img = document.querySelector('img');
console.log(img.getAttribute('src'));
💬 获取 <img>
的"src"属性值。
🧱 修改属性
less
img.setAttribute('src', 'new-tea.jpg');
💬 换新图啦~(把"草莓奶茶"照片换成"抹茶奶茶")。
🧍♀️ 修改样式
ini
let box = document.querySelector('.box');
box.style.color = 'pink';
box.style.background = 'white';
🍓 类比:
店员换新制服:粉色字体 + 白色背景。 (也就是通过 JS 改 CSS)
☀️ 第 20 页:BOM(浏览器对象模型)
🌎 什么是 BOM?
BOM(Browser Object Model)是浏览器提供的"窗口级对象",让你能操作整个浏览器,而不只是网页。
🌐 包括这些核心对象:
对象 | 功能 |
---|---|
window |
整个浏览器窗口本身 |
location |
当前网页的 URL 地址 |
navigator |
浏览器信息(比如 Chrome、Edge) |
screen |
屏幕大小、分辨率 |
history |
浏览记录(前进、后退) |
🪟 4.1 window 对象
所有 JS 全局变量其实都是挂在 window
上的。
javascript
window.alert('欢迎光临小可爱奶茶店!');
🍵 类比:
"window" 就像整家奶茶店的大玻璃窗------ 你能通过它"观察整个店",还能在上面贴公告牌。
🎀 小笨蛋也能秒懂总结(第16~20页)
知识点 | 记忆口诀 | 奶茶店类比 |
---|---|---|
父子兄弟节点 | DOM家谱图 | 店长和店员层级 |
修改节点内容 | 三种方式改菜单 | 文字、HTML、整页菜单 |
插入/删除节点 | append / remove | 上架新品 / 下架旧款 |
属性与样式 | get/set/style | 看/改奶茶照片和外观 |
BOM | 操作浏览器 | 整个奶茶店的"外部系统" |
🎯 一句话助记:
"DOM管店内布局,BOM管整家店; 节点有家谱,样式换新颜。"
🌸 第 21 页:BOM 继续讲解(window)
🪟 4.2 window 对象
ini
var a = 10;
console.log(window.a); // 10
💬 解释: 所有的全局变量,默认都挂在 window
这个"老大对象"上。 → 你定义的 a
实际上等价于 window.a
。
🍵 类比:
整个奶茶店的"总控台"是 window, 所有设备(冰箱、奶茶机、灯光)都登记在 window 上。 你说
a = 10
,就像往总控台上新增了一个"新开关"。
🧾 window 常见方法
javascript
window.alert('新品上架啦!');
window.confirm('确定要删除这杯奶茶吗?');
window.prompt('请输入您最爱的奶茶口味:');
方法 | 功能 | 奶茶店类比 |
---|---|---|
alert() | 弹出提示 | 公告板贴出"新品上架"通知 |
confirm() | 确认操作 | 问顾客"确定要退单吗?" |
prompt() | 获取输入 | 让顾客填写"口味偏好" |
🌬️ 窗口操作
javascript
window.open('https://milk-tea.com');
window.close();
🍰 类比:
你点开一个新的"分店网页"(open), 再关掉(close)就像拉下卷帘门。
🍓 第 22 页:location 对象
负责记录、读取和修改"当前网址信息"。
ini
console.log(location.href); // 当前网址
location.href = 'https://www.taobao.com'; // 跳转页面
🍬 类比:
"location" 是你奶茶店的"地址牌"。
- 看地址牌(href) = 查看现在在哪个店;
- 换地址牌 = 搬去新店(跳转页面)。
🧭 location 属性总结:
属性 | 含义 | 举例 |
---|---|---|
href | 完整地址 | https://milk.com/page.html |
hostname | 主机名 | milk.com |
pathname | 路径 | /page.html |
protocol | 协议 | https: |
search | 查询参数 | ?id=123 |
🍵 小笨蛋记忆口诀:
"协议头、主机名、路径尾,问号后面参数配。"
🌼 第 23 页:navigator 对象
记录浏览器本身的信息,比如用的是 Chrome、Safari 还是 Edge。
arduino
console.log(navigator.userAgent);
输出示例:
ini
Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 Chrome/122.0 Safari/537.36
🍹 类比:
navigator
就像奶茶店的"前台员工", 你问他:"你是哪家店的?" 他回答:"Chrome 奶茶旗舰店,版本 122。"
💻 navigator 常用属性
属性 | 含义 |
---|---|
appName | 浏览器名称 |
appVersion | 版本号 |
language | 浏览器语言 |
platform | 操作系统平台 |
userAgent | 浏览器完整信息 |
🍓 类比:
这些属性就像员工证件:姓名、编号、语言、工号。
🧋 第 24 页:screen 和 history
4.5 screen(屏幕)
存储的是屏幕尺寸、可视宽高等信息。
arduino
console.log(screen.width, screen.height);
🍵 类比:
"screen" 就像你奶茶店的橱窗大小: 橱窗越大(屏幕宽),能展示的奶茶越多。
4.6 history(浏览记录)
记录你访问过的网页,可以前进或后退。
scss
history.back(); // 后退一页
history.forward(); // 前进一页
🍰 类比:
history 就是"奶茶回忆录"📖。
back()
回到上一杯喝过的奶茶;forward()
去尝下一杯。
🍑 第 25 页:== 与 === 的区别
💡 区别一览表:
运算符 | 含义 | 特点 |
---|---|---|
== |
相等(值相等) | 自动类型转换 |
=== |
全等(值与类型都相等) | 不会转换类型 |
🧠 举例讲透:
ini
1 == '1' // true(字符串自动转数字)
1 === '1' // false(类型不同)
null == undefined // true
null === undefined // false
🍬 类比:
==
就像"模糊匹配"------你说"草莓",我也当成"草莓奶茶";===
是"严格匹配"------只有"草莓奶茶"才算对,不含糊。
☕ 记忆口诀:
"双等宽松聊恋爱,三等谨慎签合同。"
(意思是: == 可能因为类型转换"将就"一下, === 必须类型、数值都一模一样,像签合同一样严谨。)
🎀 小笨蛋也能懂的总结(第21~25页)
模块 | 核心功能 | 奶茶店类比 |
---|---|---|
window | 浏览器全局对象 | 总控台 |
location | 地址栏 | 门牌号 |
navigator | 浏览器信息 | 店员身份证 |
screen | 屏幕信息 | 橱窗尺寸 |
history | 浏览记录 | 奶茶回忆录 |
== vs === | 比较规则 | 模糊恋爱 vs 严谨婚姻 |
🌟 一句话记忆:
"window 全局掌控,location 换店导航; navigator 报身份,screen 看橱窗,history 查奶茶过往。 双等将就爱,三等要真心。" ❤️
🧋第 26 页:== 和 === 再复习
🍰 图里的核心区分
==
:宽松比较,会自动转换类型。===
:严格比较,类型和值都必须相同。!=
/!==
:对应"不相等"和"非全等"。
📖 举个例子:
ini
1 == '1' // true 字符串自动转数字
1 === '1' // false 类型不同
🍬 类比:
==
就像店员说:"草莓味?草莓奶茶也行~"===
就像严格审查:"必须草莓奶茶加波霸,否则不算!"
🌸第 27 页:5.1 关于类型转换
🧮 JS 在比较时会自动"帮你转换类型"
ini
console.log(1 == true); // true
console.log(0 == false); // true
console.log('' == false); // true
💡 JS 的脑回路是这样的:
- true → 转成数字 1
- false → 转成数字 0
- 空字符串 → 也是 0
所以这些结果都是真的。
🍹 类比:
顾客点单时,服务员会自动脑补: "他说'一杯',那肯定是数量 1;他说'不要',就是 0;啥都不说就是空(0)。"
💥 常见坑点示例
arduino
console.log([] == false); // true
console.log([] == ![]); // true
解释:
[]
转换成数字 → 0;![]
是 false;- 0 == 0 → true。
🍵 类比:
空杯子和没杯子,在店员眼里都"算空"~ 结果就被认成一样。
🍑第 28 页:5.2 空字符串转换 & 5.3 对象转换
空字符串的比较
ini
console.log('' == 0); // true
console.log('' === 0); // false
💬 ''
会自动变成数字 0 → 所以 ==
觉得一样,===
说类型不同。
🍰 类比:
"0 元优惠券"和"空白优惠券",服务员模糊地认为一样(==); 但经理查账会说:不一样的单据(===)。
对象比较
ini
let a = {};
let b = {};
console.log(a == b); // false
console.log(a === b); // false
💬 解释: 对象比较时,看的是引用地址,不是内容。 a、b 两个对象在不同的内存位置。
🍵 类比:
你做了两杯一模一样的奶茶,但放在不同桌上, 看起来一样,其实"杯号"不同。
🌷第 29 页:5.4 小结 & 6.1 typeof
💡 小结口诀:
"值类型比值,引用比址; == 会转换,=== 不客气。"
🧭 6.1 typeof 操作符
用来判断 数据类型。
csharp
typeof 123 // 'number'
typeof '奶茶' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof null // ❗ 'object'(历史遗留 bug)
typeof {} // 'object'
typeof [] // 'object'
typeof function(){} // 'function'
🍬 总结成一句话:
typeof 可以看出"是什么口味"的奶茶, 但遇到空杯(null)会认错成"奶茶杯(object)"😂
📦 小笨蛋速记表:
值 | typeof 结果 | 小故事 |
---|---|---|
123 | number | 数字奶茶编号 |
'奶茶' | string | 奶茶标签 |
true | boolean | 点单开关 |
undefined | undefined | 还没做好的奶茶 |
null | object ❗ | 空杯(被误认成对象) |
[] | object | 奶茶组合盒 |
function | function | 奶茶制作机 |
🌼第 30 页:6.2 instanceof
判断一个对象是否是某个构造函数的实例。
javascript
let arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
💬 原理: instanceof 是沿着原型链往上查,看是否能找到对应的构造函数。
🍰 类比:
店员小明是"饮品部(Array)"的员工, 但饮品部又属于"总店(Object)"。 所以:
- 是 Array 的实例 ✅
- 也是 Object 的一员 ✅
☕ typeof vs instanceof 区别总结:
比较方式 | 用于 | 返回 | 特点 |
---|---|---|---|
typeof | 基本类型 | 字符串 | 判断简单类型(除了 null) |
instanceof | 引用类型 | 布尔值 | 判断对象属于哪种"家族" |
🍹 口诀记忆:
"typeof 查口味,instanceof 查家族。"
🎀 小可爱总结(第26~30页)
知识点 | 一句话记忆 | 奶茶店例子 |
---|---|---|
== / === | 宽松恋爱 vs 严谨婚姻 | "差不多"也算 vs 必须完全一致 |
类型转换 | JS 自动脑补 | 空杯 ≈ 没杯 |
typeof | 看口味 | 判断奶茶类型 |
instanceof | 查家族 | 判断是饮品部还是甜品部 |
💡 超级总结口诀:
"双等将就爱,三等要真心; typeof 看奶茶口味,instanceof 查员工部门; 值比值,址比址,空杯容易出事。"
🧋 第 31 页:instanceof
实战讲解
javascript
function Car() {}
let myCar = new Car();
console.log(myCar instanceof Car); // true
console.log(myCar instanceof Object); // true
💡解释:
myCar
是用Car
这个构造函数造出来的。- 所以
myCar
是Car
的实例; - 而所有对象最终都继承自
Object
,因此它也是Object
的实例。
🍰类比:
"Car" 是【奶茶制作机的蓝图】;
myCar
是【做好的那台机器】; 所有机器都属于【设备部(Object)】。
🪜 instanceof 的工作原理图
它会从对象的原型链一路往上查:
对象的
__proto__
→ 构造函数的prototype
→ 再往上找直到 Object.prototype。
📦类比:
就像从"店员"往上查找: 店员 → 店长 → 区经理 → 总公司。 如果在任一层找到了"身份证模板",那就返回 true。
🌸 第 32 页:6.3 复制(拷贝)
有时候我们想"复制一个对象",但是对象存在"引用传递"问题:
ini
let obj1 = { name: '小可爱' };
let obj2 = obj1;
obj2.name = '小笨蛋';
console.log(obj1.name); // 小笨蛋
💬解释: obj2
并不是新的对象,只是复制了引用地址。 改一个,另一个也变。
🍹类比:
你把"奶茶配方表"的链接发给别人,他改了糖量,你的配方也跟着变~😱
✨浅拷贝与深拷贝
拷贝方式 | 含义 | 常见方法 |
---|---|---|
浅拷贝 | 只复制第一层 | Object.assign() 、... 展开符 |
深拷贝 | 连内部对象也复制 | JSON.parse(JSON.stringify(obj)) |
🍵类比:
浅拷贝就像"复制菜单目录"; 深拷贝是"连奶茶配方都重新抄一份"。
🧠 第 33 页:7. JavaScript 原型与原型链
🧩 原型(Prototype)是啥?
每个函数在创建时,JS 都会自动给它加一个属性
prototype
。 这个属性指向一个对象,这个对象里保存着"共享的属性和方法"。
举例:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi,我是 ' + this.name);
};
let p1 = new Person('小可爱');
p1.sayHi(); // 输出:Hi,我是 小可爱
🍬解释:
Person
是"模板"(构造函数);prototype
是模板的"说明书";p1
是根据模板造出的"员工";- 员工会自动带上说明书里的技能。
🍓类比:
你是"奶茶培训学校校长(Person)"; 每个毕业生(p1、p2)都学会相同技能 sayHi(), 因为都读了一样的"培训手册(prototype)"。
🧋 第 34 页:原型链(Prototype Chain)
javascript
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
💡解释:
JS 会一层层往上查找属性,直到最顶层
Object.prototype
。
🍵类比:
小可爱(p1) → 奶茶学院(Person) → JS 总部(Object) → 没上级了(null)。
🧭 原型链查找机制
当你访问 p1.sayHi
时,JS 会:
- 先查 p1 自己有没有;
- 没有就查它的原型;
- 再没有就一直往上找,直到顶层。
🍹类比:
员工不会调奶茶机?那去问店长; 店长不会?问区域经理; 再不会?问总部; 总部都没教?那就"undefined"了 😆。
🌷 第 35 页:可视化理解原型链
代码示例:
javascript
function Person() {}
let person1 = new Person();
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
🧩解释:
- 构造函数的 prototype 有个
constructor
属性指回自己; - 实例的
__proto__
指向构造函数的 prototype。
🍰类比:
奶茶培训学院(Person)有一本教材(prototype), 教材上注明"作者:Person"; 学生(person1)的说明书(proto)引用了这本教材。
💡 小笨蛋超记口诀(第31~35页)
概念 | 一句话总结 | 奶茶店类比 |
---|---|---|
instanceof | 判断家族关系 | 员工属于哪个部门 |
浅拷贝 | 复制目录 | 只抄菜单标题 |
深拷贝 | 完整复制 | 连奶茶配方都重写 |
prototype | 模板说明书 | 培训手册 |
原型链 | 一层层往上找 | 员工 → 店长 → 总公司 |
🎀 超萌终极口诀
"instanceof 查家谱, 浅深拷贝别混淆; prototype 是培训书, 原型链上层层找。"
🧋第 36 页:原型链的全图理解
图上是一张"大型 JS 家族树",主要说明:
- 所有函数(包括构造函数)都是
Function
的实例。 - 所有对象的最终祖宗都是
Object.prototype
。 - 顶端是
null
------ 没有再往上的人了。
🧱 你可以这样理解层级:
角色 | 说明 | 类比 |
---|---|---|
Person | 构造函数(奶茶学院) | 教你做奶茶 |
person | 实例对象(学员) | 毕业的奶茶师 |
Person.prototype | 原型(教材) | 学员共享技能 |
Object.prototype | JS 的终极祖宗 | 总部培训手册 |
📘 示例代码:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('你好,我是 ' + this.name);
};
let p1 = new Person('小可爱');
p1.sayHi(); // 输出:你好,我是 小可爱
📖 解释:
p1
自己没有sayHi
;- JS 会顺着原型链去找 → 找到
Person.prototype
; - 如果还没有,就再去
Object.prototype
; - 一直到顶端才停止。
🍓 类比:
奶茶师(p1)不知道做某种新口味 → 去查学院教材(prototype); 教材里没写?再问总公司(Object)!
🌸第 37 页:原型链的关系验证
javascript
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
📚 结论:
- 原型链是一个"逐级上溯"的结构;
null
表示没有再往上的祖先。
🍵 类比:
小可爱(p1) → 奶茶学院(Person) → 总部(Object) → 天花板(null)。
🪜 小笨蛋记忆口诀:
"对象有原型,原型也有原型, 原型尽头是 null,总公司没人管。"
🍰第 38 页:作用域链的理解
JS 中作用域是"变量能被访问到的范围"。
📖 分三类:
- 全局作用域:所有地方都能访问。
- 函数作用域:只在函数内部有效。
- 块级作用域 :
let
、const
在{}
内有效。
🍹 类比:
- 全局作用域:奶茶总部公告,所有分店都能看;
- 函数作用域:某家分店的员工守则;
- 块级作用域:当天的临时活动通知。
📘 示例
javascript
let name = '总部'; // 全局
function branch() {
let name = '奶茶店A'; // 局部
console.log(name);
}
branch(); // 奶茶店A
console.log(name); // 总部
🍬 解释: 内部作用域可以访问外部,但外部不能反过来访问内部。
☕第 39 页:作用域链机制
当 JS 访问变量时,会:
- 先在当前作用域找;
- 找不到就往上一级;
- 一直查到全局;
- 如果都没有,就报错。
ini
let a = '总部';
function shop() {
let a = '奶茶店';
function employee() {
console.log(a);
}
employee();
}
shop(); // 奶茶店
🍓 类比:
员工问"今天喝什么口味?" 自己分店有菜单 → 就用分店的; 没有才去问总部。
⚙️ 块级作用域
ini
if (true) {
let x = 10;
}
console.log(x); // ❌ 报错
💬 解释: let
定义的变量只在 {}
内可用。
🍵 类比:
活动期间的"买一送一券", 离开当天活动就作废。
🧠第 40 页:作用域综合演示
ini
var x = 10;
function foo() {
console.log(x);
var x = 20;
}
foo(); // undefined
💬 解释:
- JS 在执行前会"提升变量声明(hoisting)",
- 所以
var x
在函数里被提前声明但未赋值; - 输出时它是
undefined
。
🍰 类比:
店员问:"今天有草莓吗?" 仓库(函数)先登记了草莓这件事(声明)但还没送货(赋值), 所以此时"库存为空"。
🎀 小笨蛋终极总结(第36~40页)
概念 | 一句话记忆 | 奶茶店比喻 |
---|---|---|
原型链 | 层层继承 | 员工 → 店长 → 总部 |
prototype | 共用技能本 | 培训教材 |
作用域 | 变量可见范围 | 告示适用范围 |
作用域链 | 一层层查找变量 | 从店员问到总部 |
变量提升 | 先登记后发货 | 草莓先记账还没送到 |
🍓 超萌口诀收尾!
"原型链上查家谱,作用域里找口味; 局部外部层层叠,未赋先提是空杯。"
🧋第 41 页:作用域链回顾
来看这段代码👇
ini
var a = 2;
function foo() {
var b = 3;
function bar() {
var c = 4;
console.log(a + b + c);
}
bar();
}
foo(); // 输出 9
💬 解释: JS 查找变量时是"从内往外"找的。 bar() → 先看自己有没有 a
、b
、c
。 找不到 a
→ 去 foo() 外层 → 再找全局。
🍵 类比:
店员(bar)要做奶茶:
- 先看自己柜台有没有材料;
- 没有就去门店仓库(foo);
- 再不行就打电话总部(全局)。
🌱 图示说明
图上展示:
- 每个函数都有自己的"作用域气泡";
- 外层函数就是"上一级仓库";
- 它们连接起来形成 作用域链(Scope Chain) 。
🍬 口诀:
"函数套函数,作用域像洋葱; 从内往外剥,总能找到葱。"
🌸第 42 页:作用域链图解(办公楼模型)
图里是一个"高楼层结构":
- 每层代表一个作用域;
- 顶楼是全局作用域;
- 内层函数越深 → 楼层越高。
📖 小笨蛋记法:
想象变量是一杯奶茶 🍹 你在 7 楼想找奶茶,先看自己桌上有没有; 没有就去 6 楼 → 5 楼 → 一直找到 1 楼总部。
📘 代码解释
ini
var x = 10;
function outer() {
var y = 20;
function inner() {
var z = 30;
console.log(x + y + z);
}
inner();
}
outer(); // 输出 60
📍执行顺序:
- inner 自己的作用域(z = 30)
- outer 的作用域(y = 20)
- 全局作用域(x = 10)
🍰 类比:
店员(inner)要做"特调奶茶", 自己柜台有糖浆(z), 从楼下仓库拿茶叶(y), 最后总部提供牛奶(x)。
☀️第 43 页:作用域链小结
级别 | 名称 | 范围 |
---|---|---|
1 | 全局作用域 | 所有函数外部 |
2 | 函数作用域 | 每个函数独立 |
3 | 块级作用域 | {} 内生效(let/const) |
💡 JS 在"定义函数时"就决定好作用域,而不是运行时才决定。 👉 这就是 "词法作用域(Lexical Scope)" 。
🍹 类比:
店员入职那天,公司就决定了他能进哪些仓库。 不是等他去拿材料时才临时决定的。
🧠第 44 页:this 对象(灵魂考点登场)
this
就是当前执行环境下的对象, 取决于"函数被谁调用",而不是定义时。
💻 基础例子:
ini
var name = '总部';
function sayHi() {
console.log(this.name);
}
const shop = {
name: '奶茶店A',
sayHi: sayHi
};
sayHi(); // 输出:总部
shop.sayHi(); // 输出:奶茶店A
📖 解释:
- 第一行
sayHi()
是由全局环境调用 → this 指向window
; - 第二行
shop.sayHi()
是由对象调用 → this 指向shop
。
🍓 类比:
同一句话 "欢迎光临~"
- 在总部喊:代表总部欢迎;
- 在分店喊:代表分店欢迎。 所以 this 取决于"谁喊的"!
🌷第 45 页:this 指向的 4 条核心规则
💡 1. 默认绑定(普通函数调用)
scss
function show() {
console.log(this);
}
show(); // 浏览器中输出 window
👉 没有指定对象,this 默认是全局对象(浏览器里是 window)。
🍹 类比:
没写发件人名字,系统默认认为是"总部"发的。
💡 2. 隐式绑定(对象调用)
javascript
const tea = {
name: '奶茶A',
getName() {
console.log(this.name);
}
};
tea.getName(); // 奶茶A
👉 调用者是 tea 对象 → this = tea。
🍰 类比:
店员(this)属于哪个分店,就说那家店的口味。
💡 3. 显式绑定(call/apply/bind)
javascript
function greet() {
console.log('Hi, ' + this.name);
}
const shop = { name: '奶茶店B' };
greet.call(shop); // Hi, 奶茶店B
💬 .call()
手动指定 this。 🍬 类比:
像你强制让总部的广播系统播放"奶茶店B"的口号。
💡 4. new 绑定(构造函数)
ini
function Person(name) {
this.name = name;
}
const p = new Person('小可爱');
console.log(p.name); // 小可爱
👉 使用 new
时,this 会绑定到"新建的对象"上。
🍵 类比:
"new" 就像开了一个新分店,新店就成了 this!
💫 小笨蛋专属总结(第41~45页)
概念 | 一句话总结 | 奶茶店类比 |
---|---|---|
作用域 | 变量能访问的区域 | 奶茶仓库范围 |
作用域链 | 多层作用域查找 | 从分店查到总部 |
词法作用域 | 定义时确定范围 | 入职时发的权限卡 |
this | 谁调用指向谁 | 谁喊"欢迎光临"就代表谁 |
call/apply/bind | 手动指定 this | 强制让总部喊出分店的口号 |
new 绑定 | 构造函数绑定 | 开新分店,新 this 诞生! |
🎀 终极口诀
"作用域链像仓库,层层往上找; this 看谁喊话,谁喊谁代表。 new 开新店,call 改台词, JS 虽抽象,其实奶茶味十足~🍹"
🧋第 46 页:this
的四种绑定方式复习
这几页主要讲的就是------ this 到底是谁?取决于"谁喊话"!
你可以想成:this 是函数执行时的"身份牌" 。 谁在调用函数,this 就代表谁!
🍰 9.2.2 隐式丢失(this 丢了)
ini
function show() {
console.log(this.name);
}
var name = '总部';
const obj = { name: '分店A', show };
obj.show(); // 分店A
const f = obj.show;
f(); // 总部(this 丢失)
💬解释:
obj.show()
→ 调用者是obj
,所以 this = obj;f()
→ 函数单独被调用,就回到默认绑定 → this = window。
🍹类比:
奶茶店员工(show)平时在分店上班(this = 分店A)。 但有一天他辞职在家做兼职(脱离 obj),那就成了自由人(this = window)。
🌸第 47 页:隐式丢失的陷阱
javascript
var name = '总部';
const shop = {
name: '奶茶店B',
getName() {
console.log(this.name);
}
};
setTimeout(shop.getName, 1000); // 输出:总部
💡原因: setTimeout
里的回调函数是"普通调用", 不是由 shop 直接调用的,this 丢了!
🍬类比:
总部让你"1秒后喊出你的名字", 结果你忘了自己在哪家分店工作,就喊"总部!"😂
✅ 解决办法:
scss
setTimeout(() => shop.getName(), 1000);
👉 箭头函数不改变 this(后面讲)
☕第 48 页:new
绑定(构造函数)
ini
function Tea(name) {
this.name = name;
}
let cup = new Tea('草莓奶茶');
console.log(cup.name); // 草莓奶茶
💬解释:
- 使用
new
时,JS 会自动创建一个新对象; - 并把 this 绑定到这个新对象上;
- 最后自动返回它。
🍵类比:
"new" 就像开了一家新分店。 新分店会挂着自己的门牌(this),独立存在。
🧠第 49 页:箭头函数中的 this
dart
const shop = {
name: '分店C',
show: () => {
console.log(this.name);
}
};
shop.show(); // undefined(不是分店C!)
💬解释: 箭头函数不会创建自己的 this, 它的 this 是"外层作用域"的 this。
🍬类比:
箭头函数是"实习生"------没有自己的工牌, 它用的是外层经理的工牌。 如果外层是总部环境 → 它拿的是总部的工牌。
📘 正确写法
javascript
const shop = {
name: '分店C',
show() {
console.log(this.name);
}
};
shop.show(); // 分店C ✅
🍓第 50 页:事件绑定中的 this
javascript
document.querySelector('button').addEventListener('click', function() {
console.log(this); // <button> 元素本身
});
💡解释:
- 普通函数绑定事件 → this 指向触发事件的 DOM 元素。
🍵类比:
顾客(click)点了哪一杯奶茶(button), this 就代表那杯奶茶本身。
⚡箭头函数事件绑定
javascript
document.querySelector('button').addEventListener('click', () => {
console.log(this); // window
});
💬箭头函数没有自己的 this,所以 this 来自外层(通常是 window)。
🍹类比:
实习生负责点单(箭头函数), 但他没有"杯子编号"概念, 所以直接指到总部去了(window)。
🧋第 51 页:this 四种绑定规则复习
上一章(第 45~50 页)讲了基础,现在这页讲的是:
✅ 默认绑定 ✅ 隐式绑定 ✅ 显式绑定(call/apply/bind) ✅ new 绑定
🧱 1️⃣ 默认绑定(普通函数)
javascript
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 输出 2(浏览器中是 window.a)
💬 没有任何修饰的函数调用 → this 默认指向全局对象。
🍵 类比:
如果没人明确点名是谁喊话,就默认认为是"总部"发的通知。
🧱 2️⃣ 隐式绑定(对象调用)
javascript
function foo() {
console.log(this.a);
}
var obj = { a: 2, foo: foo };
obj.foo(); // 输出 2
💬 谁点的 .foo()
,this 就指向谁。 🍰 类比:
"奶茶店 A" 员工喊"我最棒" → 那代表奶茶店 A,不是总部。
🧱 3️⃣ 显式绑定(call/apply/bind)
javascript
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 输出 2
💬 call()
/ apply()
/ bind()
可以强制修改 this。 🍓 类比:
总部(window)拿着麦克风说:"请奶茶店 A 来喊这句话!" → 强行切换发言人。
🧱 4️⃣ new 绑定
ini
function Foo(a) {
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); // 2
💬 使用 new
创建新对象时:
- 创建一个新空对象;
- 把 this 绑定到这个新对象;
- 自动返回它。
🍹 类比:
"new" 就像开了一家新的奶茶分店 🍵, 新店(this)有自己独立的库存和菜单。
🌸第 52 页:箭头函数中的 this
ini
function outer() {
return () => {
console.log(this.a);
};
}
var obj = { a: 2 };
var foo = outer.call(obj);
foo(); // 输出 2
💬 箭头函数不会自己绑定 this,它会继承外层的 this。
🍰 类比:
箭头函数像"学徒", 没有自己的工作证(this),只能用师傅的。
📘 坑点对比
ini
var a = 10;
var obj = {
a: 20,
fn: () => {
console.log(this.a);
}
};
obj.fn(); // 输出 10
💬 因为箭头函数的 this 是定义时绑定,不是运行时! 所以这里 this 是 window,而不是 obj。
🍵 类比:
学徒(箭头函数)不是听当前店长的,而是永远听"总部"的命令。😂
☀️第 53 页:new 与普通函数的区别
ini
function Foo(name) {
this.name = name;
}
let a = Foo('奶茶A'); // 普通调用
let b = new Foo('奶茶B'); // 构造调用
💬 区别:
调用方式 | this 指向 | 返回值 |
---|---|---|
普通函数调用 | 全局对象(window) | undefined |
new 调用 | 新建对象 | 这个对象本身 |
🍓 类比:
普通调用:总部代工,产品归总部。 new 调用:开新分店,产品归新店。
🧠第 54 页:new 操作符做了什么
ini
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person('Tom', 20);
💡 new 操作符做的 4 件事:
1️⃣ 创建一个新空对象 {}
; 2️⃣ 把新对象的原型指向构造函数的 prototype; 3️⃣ 把构造函数的 this 绑定到新对象上; 4️⃣ 如果函数没有返回其他对象,则自动返回这个新对象。
🍵 类比:
"new" = 新开奶茶店:
- 建新店(空对象)
- 店长培训(绑定 prototype)
- 店内设置菜单、装修(赋值 this)
- 新店开始营业(返回实例)
📘 代码演示
ini
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log('Hi, 我是 ' + this.name);
};
const p1 = new Person('小可爱', 18);
p1.sayHi(); // Hi, 我是 小可爱
🍰 类比:
新分店(p1)开业 → 会使用总部的"招呼语模板(sayHi)"。
🌷第 55 页:可视化理解 new 流程
图中展示 new 的四个阶段(总结成奶茶故事版)👇:
阶段 | JS 行为 | 奶茶类比 |
---|---|---|
① | 创建新对象 | 新奶茶店开张 |
② | 连接原型 | 给店铺挂上总部培训手册 |
③ | 绑定 this | 店长接管店铺 |
④ | 返回对象 | 店铺正式营业 🎉 |
🎀 小可爱总结(第51~55页)
概念 | 一句话记忆 | 奶茶店类比 |
---|---|---|
this 默认绑定 | 谁都没喊就是总部 | 默认 window |
this 隐式绑定 | 谁点的谁说话 | obj.fn() |
this 显式绑定 | 强制切换发言人 | call/apply |
箭头函数 | 没自己 this,继承外层 | 学徒听师傅的 |
new 操作符 | 新建对象并绑定 | 新开奶茶店 |
💡 终极口诀
"谁喊谁代表,call 改发言人; new 开新店铺,箭头跟师傅干; 四步造分店,prototype 挂菜单。"
🧋第 56 页:手写 new
操作符(模拟实现)
💻 new 是什么?
当我们执行:
ini
function Person(name, age) {
this.name = name;
this.age = age;
}
let p = new Person('小可爱', 18);
JS 内部其实做了 4 步事👇:
1️⃣ 创建一个空对象 {} 2️⃣ 把这个对象的原型指向 Person.prototype 3️⃣ 绑定 this 到这个新对象上,并执行构造函数 4️⃣ 如果构造函数没有返回对象,则返回这个新建对象
🍵 类比:
"new" 就像 新开一家奶茶分店: ① 先建店面(空对象) ② 给它挂上总部的培训手册(prototype) ③ 店长(this)负责装修和配置菜单 ④ 没意外就开门营业(返回对象)
🧱 手写 new 代码实现
javascript
function myNew(fn, ...args) {
const obj = {}; // ① 创建空对象
obj.__proto__ = fn.prototype; // ② 链接原型
const result = fn.apply(obj, args); // ③ 执行构造函数并绑定 this
return result instanceof Object ? result : obj; // ④ 返回对象
}
🍰 类比解释:
obj
是"新开的奶茶店";obj.__proto__ = fn.prototype
让它能用总部的配方书;fn.apply(obj, args)
店长装修菜单;- 最后返回新开的奶茶店实例。
🌸第 57 页:call / apply / bind 的作用与区别
方法 | 是否立即执行 | 参数传递方式 | 作用 |
---|---|---|---|
call | ✅ 立即执行 | 参数一个个传 | 改变 this |
apply | ✅ 立即执行 | 参数数组传 | 改变 this |
bind | ❌ 不立即执行 | 参数一个个传 | 返回新函数 |
🍬 类比:
call
:立刻喊总部广播宣传。📢apply
:让总部一次性播放多个分店的公告。📜bind
:先录好音,等需要时再播放。🎙️
🧱 例子演示(第57页)
javascript
function greet(gift) {
console.log(`${this.name} 收到 ${gift}`);
}
const person = { name: '小可爱' };
greet.call(person, '草莓奶茶'); // 小可爱 收到 草莓奶茶
greet.apply(person, ['波霸奶茶']); // 小可爱 收到 波霸奶茶
const newFn = greet.bind(person, '珍珠奶茶');
newFn(); // 小可爱 收到 珍珠奶茶
🍵 记忆口诀:
call 是"单点直呼";apply 是"套餐传递";bind 是"预约执行"。
☀️第 58 页:手写 call、apply、bind 实现
✨ 手写 call
ini
Function.prototype.myCall = function (context, ...args) {
context = context || window;
context.fn = this; // 暂时把函数放进 context 里
const result = context.fn(...args);
delete context.fn; // 清除函数
return result;
};
🍹 类比:
你(this)临时借用了别的奶茶店的广播系统(context), 播完广告(函数执行)后再拔掉插头(删除 fn)。
✨ 手写 apply
ini
Function.prototype.myApply = function (context, args) {
context = context || window;
context.fn = this;
const result = args ? context.fn(...args) : context.fn();
delete context.fn;
return result;
};
📘 区别:apply 的参数是数组形式传入。
✨ 手写 bind
javascript
Function.prototype.myBind = function (context, ...args) {
const fn = this;
return function (...innerArgs) {
return fn.apply(context, [...args, ...innerArgs]);
};
};
🍰 类比:
你把总部的"广告词"录成语音(返回新函数), 以后店长随时播放(延迟执行)。
🧠第 59 页:call / apply / bind 的区别表(总结页)
方法 | 是否立即执行 | 参数格式 | 返回结果 |
---|---|---|---|
call | ✅ 是 | 单个参数 | 结果 |
apply | ✅ 是 | 数组 | 结果 |
bind | ❌ 否 | 单个参数 | 新函数 |
📍小笨蛋口诀:
"call 立刻喊,apply 队形喊,bind 等会儿喊。"
🌷第 60 页:总结 + 延伸思考
💡 面试高频问:
"请你实现一个简单的 bind() 函数。"
你只要记得:
- bind 不会立刻执行;
- 它返回一个新函数;
- 新函数执行时 this 指向固定对象;
- 还能预置参数。
📘 面试示例:
javascript
Function.prototype.simpleBind = function(context, ...args) {
const fn = this;
return function(...innerArgs) {
return fn.apply(context, args.concat(innerArgs));
};
};
🌈 一句话总结(第56~60页)
知识点 | 含义 | 奶茶店比喻 |
---|---|---|
new 操作符 | 创建对象的过程 | 新开奶茶分店 |
call | 改变 this,立即执行 | 总部广播切换发言人 |
apply | 改变 this,数组参数 | 一次性播放多条公告 |
bind | 返回新函数,延迟执行 | 录音留待下次播放 |
🎀 小笨蛋终极口诀
"new 开分店,四步搞定; call、apply、bind 三兄弟, 一个立刻喊,一个成群喊,一个改天喊。🎤"
🧋第 61 页:代码执行机制 & 执行上下文
💡 什么是执行上下文?
JS 在运行代码时,每当进入一个环境(比如全局 / 函数) , 它都会创建一个"执行上下文(Execution Context)"。
执行上下文包含三部分: 1️⃣ 变量对象(Variable Object) --- 存储变量、函数声明等; 2️⃣ 作用域链(Scope Chain) --- 用于查找变量; 3️⃣ this 指向 --- 谁在调用。
🍵 类比:
"执行上下文"就像一个奶茶工作台:
- 变量对象:桌上摆的原料;
- 作用域链:能借到的外部仓库;
- this:当班的奶茶师是谁。
📘 示例代码
ini
var a = 10;
function foo() {
var b = 20;
function bar() {
var c = 30;
console.log(a + b + c);
}
bar();
}
foo(); // 输出 60
🍰 解释:
- JS 先进入全局上下文(厨房总控台);
- 执行到
foo()
时,创建foo
的上下文(新工作台); - 执行到
bar()
,又开一张小桌(bar 的上下文); - 打印完结果后,bar 桌子收掉,回到 foo 桌,再回到全局。
📦 图示:每个函数运行时会"压入执行栈"(像奶茶杯叠起来), 执行完毕就"弹出栈顶"(喝完收掉)。
🌸第 62 页:执行栈(Execution Stack)
所有执行上下文是按照 "先进后出(LIFO)" 的方式存放在执行栈中。
💻 举例:
sql
function first() {
console.log('🥤 first');
second();
}
function second() {
console.log('🧋 second');
}
first();
console.log('🏁 end');
执行顺序:
顺序 | 事件 | 栈状态 |
---|---|---|
1 | 进入全局 | [Global] |
2 | 调用 first | [Global, first] |
3 | 调用 second | [Global, first, second] |
4 | 执行完 second | [Global, first] |
5 | 执行完 first | [Global] |
6 | 执行完毕 | 栈空 |
🍹 类比:
奶茶师做单的顺序------ 第一单(first)点了要调用第二单(second), 做完第二单再回来继续第一单,最后才清空。
☀️第 63 页:执行上下文的生命周期
执行上下文一共经历三个阶段:
1️⃣ 创建阶段(Creation Phase)
- 创建变量对象(VO)
- 建立作用域链
- 确定 this 指向
2️⃣ 执行阶段(Execution Phase)
- 逐行执行代码,给变量赋值、调用函数等
🍓 类比:
- 创建阶段:奶茶店"准备原料、工具、菜单";
- 执行阶段:开始实际"做奶茶、出单"。
💻 举例
ini
console.log(a); // undefined
var a = 10;
console.log(a); // 10
📖 执行顺序:
- 创建阶段:
a
被声明但未赋值(默认 undefined); - 执行阶段:给
a
赋值为 10。
🍵 类比:
点单还没开始做的时候,奶茶杯已准备好(声明), 但杯子里还没倒奶茶(赋值) → 所以一开始是空的 undefined。
🧠第 64 页:变量提升与函数提升
JS 在创建阶段会把所有变量声明 与函数声明提前到顶部。
📘 示例
javascript
console.log(a); // undefined
var a = 10;
foo(); // OK ✅
function foo() {
console.log('函数提升成功');
}
💡解释:
- 变量声明被提升,但赋值不会;
- 函数声明整体提升(所以能提前调用)。
🍰 类比:
店长先在白板上写好菜单(声明), 但奶茶还没做(赋值)。 函数像"预制好的奶茶",随时可以拿来用。
🧱 函数表达式不会被提升
javascript
sayHi(); // ❌ 报错
var sayHi = function() {
console.log('hi~');
};
💬 因为这是"变量声明 + 函数赋值", 声明会提升,但赋值在执行阶段才发生。
🍹 类比:
你先说"我要雇个员工",但真正员工还没上班。 所以执行时会找不到人。
🌷第 65 页:执行上下文小结图
💡 三层概念:
名称 | 含义 | 奶茶类比 |
---|---|---|
执行上下文 | 代码运行环境 | 奶茶工作台 |
执行栈 | 上下文的堆叠顺序 | 点单堆叠系统 |
生命周期 | 从准备到完成的过程 | 从备料到出单 |
📜 总结口诀:
"先建工作台,再堆栈出单; 声明先到场,赋值慢半拍; 函数提前泡好茶,表达式还在请人。"
💎 小笨蛋专属总结(第61~65页)
概念 | 一句话记忆 | 奶茶店比喻 |
---|---|---|
执行上下文 | 每个函数的"运行环境" | 奶茶工作台 |
执行栈 | 上下文的执行顺序 | 奶茶订单堆叠 |
生命周期 | 创建→执行→销毁 | 备料→做单→送出 |
变量提升 | 声明提前,赋值靠后 | 先写菜单,后做奶茶 |
函数提升 | 可提前调用 | 预制奶茶随时取用 |
🌈 终极口诀
"上下文是厨房台,执行栈是订单排; 提升只是登记名,真正赋值要后来。"
🧋第 66 页:执行上下文更深入理解
🧠 执行上下文栈(Execution Context Stack)
我们之前知道每个函数执行时都会创建一个"执行上下文"。 所有的执行上下文,会按顺序压入一个栈(stack)中,栈底是全局上下文。
sql
function first() {
console.log('🍓 first');
second();
}
function second() {
console.log('🍵 second');
}
first();
console.log('🏁 end');
🧩 执行顺序:
- 进入全局上下文(创建 Global Context)
- 调用
first()
→ 入栈 - 执行到
second()
→ 入栈 second()
执行完 → 出栈first()
执行完 → 出栈- 程序结束 → 栈空
🍵 类比奶茶店:
点单系统像"订单栈":
- 先做的奶茶压在下面;
- 后下单的排在上面;
- 必须先完成上面的订单才能继续下面的。
💡 图示理解:
📊 图中绿色方框表示不同的执行上下文:
- Global(全局)永远在最底部;
- 当前执行的函数上下文在最上面。 当函数结束后,JS 会"弹出"这层上下文。
🍓 小口诀:
"先进后出,上桌先收。" (奶茶要先完成上面的新单,才能轮到旧单。)
🌸第 67 页:执行顺序的可视化理解
💻 示例
javascript
console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');
输出结果:
css
A
C
B
💬 为什么 B
最后? 因为:
- JS 是单线程执行;
setTimeout
的回调会进入任务队列(Event Queue) ;- 主线程执行完当前任务(A、C)后,再执行队列任务。
🍰 奶茶店类比:
前台点单员(主线程)只能一次接待一位顾客; 叫号机(任务队列)会记下还没轮到的顾客(setTimeout), 前面客人都服务完后,再喊"下一位~!"
☀️第 68 页:事件模型(Event Model)
💡 概念:
事件模型决定了浏览器中事件(比如点击、输入、滚动)是如何被捕获与触发的。
分三种阶段: 1️⃣ 捕获阶段 (从外到内) 2️⃣ 目标阶段 (事件触发的元素本身) 3️⃣ 冒泡阶段(从内往外)
🍵 类比:
奶茶店有三个层级:
- 店长(捕获阶段)→ 看全场;
- 柜台(目标阶段)→ 处理点单;
- 店员(冒泡阶段)→ 把消息传回管理层。
📘 示例
xml
<div id="outer">
<button id="inner">点我!</button>
</div>
<script>
document.getElementById('outer').addEventListener('click', () => {
console.log('外层 div 被点击');
}, true); // 捕获阶段
document.getElementById('inner').addEventListener('click', () => {
console.log('按钮被点击');
}, false); // 冒泡阶段
</script>
输出顺序:
css
外层 div 被点击
按钮被点击
💡 因为 true
表示"捕获阶段"先执行!
🍹 类比:
店长(outer)先看到顾客进门 → 再由店员(inner)服务点单。
🧠第 69 页:事件捕获与冒泡机制
📘 事件流示意图
图上箭头展示三阶段:
🔽 捕获阶段:从 window → document → body → div → button
🔼 冒泡阶段:从 button → div → body → document → window
🍬 类比:
顾客从门口(window)走到收银台(button)点单, 然后信息再层层传回总部(window)。
📘 示例 2
javascript
document.body.addEventListener('click', () => console.log('Body 捕获'), true);
document.body.addEventListener('click', () => console.log('Body 冒泡'), false);
document.getElementById('btn').addEventListener('click', () => console.log('Button 被点击'));
点击按钮:
css
Body 捕获
Button 被点击
Body 冒泡
💡 "捕获先走,冒泡后到"。
🌷第 70 页:事件对象(Event Object)
每个事件都有一个 event 对象,里面记录了各种信息:
属性 | 作用 |
---|---|
target |
触发事件的元素 |
currentTarget |
正在处理事件的元素 |
type |
事件类型(如 click) |
stopPropagation() |
阻止事件冒泡 |
preventDefault() |
阻止默认行为(比如表单提交) |
🍵 类比:
event 是"点单表单":
target
:哪位顾客点的;currentTarget
:哪个店员在处理;stopPropagation
:别让消息传上去;preventDefault
:比如顾客点了饮品但不想付款按钮生效。
📘 示例
javascript
document.getElementById('btn').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('按钮点击但阻止冒泡与默认行为');
});
💬 点击按钮后:
- 不会触发父级 div 的点击事件;
- 不会执行表单默认提交。
🍰 类比:
顾客点了奶茶但告诉店员:"别通知经理,也别立刻出单。" --- 这就是
stopPropagation + preventDefault
的效果!
🎀 小可爱总结(第66~70页)
概念 | 含义 | 奶茶店比喻 |
---|---|---|
执行栈 | 函数调用顺序 | 奶茶订单堆叠 |
任务队列 | 异步任务排队 | 顾客叫号系统 |
捕获阶段 | 从外到内传递事件 | 店长观察顾客 |
冒泡阶段 | 从内到外回传事件 | 店员报告经理 |
event 对象 | 事件详情表 | 顾客点单信息表 |
🌈 终极口诀
"执行栈像订单叠,事件流像客人走; 捕获先下楼,冒泡再上楼; 阻止冒泡别传递,防止默认先暂停。"
🧋第 71 页:事件绑定与管理
💡 什么是事件绑定?
事件绑定就是"当某个元素被触发时执行一个函数"。 比如点击按钮、鼠标移入、键盘按下等。
📘 示例
xml
<button id="btn">点我喝奶茶</button>
<script>
const btn = document.getElementById('btn');
btn.addEventListener('click', function() {
alert('奶茶已下单!');
});
</script>
🍵 通俗解释:
就像顾客按下点单按钮,系统自动执行"下单函数"。
addEventListener
就是告诉系统:"这个按钮被点时,请执行这个操作。"
✨ addEventListener 的三个参数
bash
element.addEventListener(type, listener, useCapture)
参数 | 含义 | 举例 |
---|---|---|
type | 事件类型 | 'click' , 'keyup' , 'mouseover' |
listener | 触发时执行的函数 | function() {...} |
useCapture | 是否在捕获阶段执行 | true / false |
🍰 类比:
这就像奶茶店给按钮"绑定服务员":
type
:哪种操作触发;listener
:服务员做的事;useCapture
:是"进门时"服务(捕获)还是"出门时"处理(冒泡)。
🌸第 72 页:事件解绑与管理优化
💡 解绑事件 --- removeEventListener
javascript
function sayHi() {
console.log('欢迎光临小可爱的奶茶店~');
}
btn.addEventListener('click', sayHi);
btn.removeEventListener('click', sayHi);
🍬 注意:
- 必须传入同一个函数引用;
- 匿名函数是无法解绑的。
🍹 类比:
如果服务员是"实名登记"的(命名函数),可以取消排班; 如果是"临时兼职匿名的",那就找不到人取消啦~
💻 常见写法陷阱
javascript
btn.addEventListener('click', function() {
console.log('hi');
});
btn.removeEventListener('click', function() {
console.log('hi');
}); // ❌ 无效!
💬 因为这两个函数虽然长得一样,但在内存中是不同地址!
☀️第 73 页:事件管理最佳实践
1️⃣ 使用命名函数 方便解绑与维护。
2️⃣ 用事件委托(Event Delegation) 当有很多元素时,不要每个都绑定事件,而是把监听交给它们的父级。
🍓 类比:
一百位顾客排队点单,你不可能每人配一个店员, 所以让前台一个人监听所有顾客的"点击事件" → 这就是事件委托!
🧠第 74 页:事件委托(Event Delegation)
💡 概念
事件委托是指:
利用事件冒泡机制,把事件绑定在父级元素 上, 通过判断事件目标(
event.target
)来处理子元素的行为。
📘 示例
xml
<ul id="menu">
<li>珍珠奶茶</li>
<li>波霸奶茶</li>
<li>草莓奶茶</li>
</ul>
<script>
const menu = document.getElementById('menu');
menu.addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
alert('你点了:' + e.target.innerText);
}
});
</script>
🍵 输出:
点击"波霸奶茶" → 弹出 "你点了:波霸奶茶"
💬 解释:
- 事件监听绑在
ul
上; - 当点击
li
时,事件会冒泡到ul
; - 通过
e.target
判断具体点击了哪个li
。
🍰 类比:
顾客在不同的收银台点单, 店长(ul)只需监听"有人点单"这件事, 然后看看是哪种奶茶(e.target.innerText)。
🌷第 75 页:事件委托的优点与应用场景
✅ 优点
1️⃣ 提高性能(减少事件绑定数量); 2️⃣ 动态元素也能自动响应(新增的 li
无需重新绑定); 3️⃣ 管理方便(只需处理父级事件)。
🍹 类比:
店长只需要管理"点单区"整体, 不管今天多了几个新饮品窗口,都能自动响应。
💻 实际应用场景
例如电商商品列表、评论区点赞、表格点击行等:
xml
<table id="orderTable">
<tr><td>珍珠奶茶</td></tr>
<tr><td>波霸奶茶</td></tr>
<tr><td>草莓奶茶</td></tr>
</table>
<script>
const table = document.getElementById('orderTable');
table.addEventListener('click', function(e) {
if (e.target.tagName === 'TD') {
console.log('点击了:' + e.target.innerText);
}
});
</script>
🍓 这样无论表格多少行、动态加载多少商品, 都不需要重新绑事件~✨
🎀 小可爱总结(第71~75页)
知识点 | 一句话记忆 | 奶茶店类比 |
---|---|---|
addEventListener | 给元素"登记动作" | 按按钮下单 |
removeEventListener | 取消登记 | 取消员工排班 |
匿名函数解绑失败 | 不同函数地址 | 找不到临时工 |
事件委托 | 父级代理处理事件 | 店长统一管理点单 |
e.target | 事件真正触发的元素 | 点单的那位顾客 |
💎 超萌口诀:
"绑定事件要实名,解绑要同名; 委托省心又高效,店长统一管点击~🍹"
🧋第 76 页:事件委托进阶 + 动态绑定
前半页在讲事件委托的实战延伸,我们快速复习一下:
📘 示例:动态添加的元素依然能响应事件
xml
<ul id="menu">
<li>奶茶A</li>
<li>奶茶B</li>
</ul>
<script>
const menu = document.getElementById('menu');
menu.addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log('你点了 ' + e.target.innerText);
}
});
// 动态添加
const newItem = document.createElement('li');
newItem.innerText = '奶茶C';
menu.appendChild(newItem);
</script>
💬 结果:
即使 "奶茶C" 是后来添加的,也能被点击响应!
🍵 原因: 事件绑定在 ul
上,而不是每个 li
。 事件冒泡时,ul
能捕捉到点击,并判断是哪杯奶茶被点。
🍰 类比:
店长只管"有人点单"这件事,不管是哪杯奶茶。 新品奶茶上架(新增 li)也能自动被处理。
🌸第 77 页:闭包的概念登场!
💡 什么是闭包(Closure)?
闭包就是:
函数可以"记住"并访问它被创建时的作用域中的变量。
即使这个函数在外部执行,它依然能访问那个作用域的变量!
📘 示例 1
javascript
function makeTea() {
let flavor = '草莓奶茶';
return function() {
console.log('顾客点的口味是:' + flavor);
};
}
const order = makeTea();
order(); // 顾客点的口味是:草莓奶茶
🍬 解释:
makeTea
执行完后,本该销毁内部变量;- 但返回的函数"引用"了
flavor
; - JS 为了让它还能用 → 把
flavor
留下; - 这就叫 闭包。
🍹 类比:
顾客点单后离开(函数执行完), 但奶茶师(内部函数)仍记得顾客的口味。 这张"点单纸条"就是闭包!📒
☀️第 78 页:闭包的实际应用场景
闭包看似神秘,其实常用于👇:
✅ 1. 数据持久化(记忆功能)
scss
function counter() {
let count = 0;
return function() {
count++;
console.log('当前奶茶订单号:' + count);
};
}
const orderCounter = counter();
orderCounter(); // 当前奶茶订单号:1
orderCounter(); // 当前奶茶订单号:2
💬 闭包帮我们"记住"了上一次的值。
🍰 类比:
每接一单,点单机会自动加一号, 虽然顾客不同,但机器能记住上次的编号。
✅ 2. 模拟私有变量(隐藏信息)
ini
function createTeaMachine() {
let secret = '配方:波霸 + 草莓酱';
return {
show: function() {
console.log(secret);
}
};
}
const machine = createTeaMachine();
machine.show(); // 输出:配方:波霸 + 草莓酱
🍬 类比:
顾客只能通过按钮(show 方法)获取配方信息, 却不能直接打开厨房偷看"secret"。
这就是 闭包实现封装 的典型应用。
🧠第 79 页:闭包与循环(经典坑题)
面试高频问题:
css
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
结果:
3
3
3
💬 为什么?
var
没有块级作用域;- 当定时器执行时,循环已结束;
i
变成了 3。
🍵 类比:
你排了 3 个顾客的单,但奶茶师只留了一张总单, 等要做时发现全都写成"第3号顾客"😅。
✅ 解决方法 1:用 let
css
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
结果:
0
1
2
🍓 因为 let
为每次循环都创建了一个独立作用域!
✅ 解决方法 2:用闭包手动保存 i
javascript
for (var i = 0; i < 3; i++) {
(function(n) {
setTimeout(() => console.log(n), 1000);
})(i);
}
🍹 每次循环都传入一个新参数 n
, 闭包帮我们"记住"当前的 i。
🍰 类比:
每位顾客都留一张独立点单纸, 奶茶师根据那张纸做单,就不会混乱。
🌷第 80 页:闭包的注意事项与总结
⚠️ 闭包的风险:内存泄漏
闭包会让变量"长期存在",如果滥用可能导致内存不释放。
javascript
function leak() {
const bigData = new Array(100000).fill('🍵');
return function() {
console.log(bigData.length);
};
}
const fn = leak();
💬 因为 fn
引用了 bigData
,JS 不会释放这块内存。 除非手动清理引用(fn = null
)。
🍬 类比:
奶茶师一直留着过期点单纸不丢,厨房就被塞满啦!
💎 小可爱总结(第76~80页)
知识点 | 含义 | 奶茶店比喻 |
---|---|---|
闭包 | 函数记住外部变量 | 点单纸条 📒 |
应用场景 | 计数器、私有变量 | 点单机记号、配方封装 |
循环问题 | 延迟执行变量被共享 | 所有顾客都写成同一个号 |
解决方式 | 用 let 或立即执行函数 | 每人一张点单纸 |
风险 | 内存泄漏 | 纸条太多堆爆仓库 |
🎀 终极口诀:
"闭包记心头,变量不放手; 点单要留名,循环别共用; 内存要勤扫,纸条要烧掉~🧋"
🧋第 81 页:闭包的综合应用(模块封装)
这一页在讲:如何用闭包封装数据,创建"模块化结构"。
📘 示例:
ini
var Customer = (function() {
var name = '小可爱';
var orders = [];
function addOrder(item) {
orders.push(item);
}
function getOrders() {
return orders;
}
return {
name: name,
order: addOrder,
show: getOrders
};
})();
💬 调用:
vbnet
Customer.order('珍珠奶茶');
Customer.order('草莓冰沙');
console.log(Customer.show());
🧠 输出:
css
['珍珠奶茶', '草莓冰沙']
🪄 通俗解释:
这里我们用闭包 把变量 name
和 orders
"藏"起来, 外部访问不到,只能通过暴露的函数(order
、show
)来操作。
🍹 奶茶店比喻:
Customer
是一个"点单管理系统":
- 点单列表(orders)是后台数据库;
- 外部顾客不能直接改,只能通过按钮(order / show)操作;
- 这样就防止"乱改订单"啦 ✅。
📎 小记忆法:
闭包 + 返回对象 = 简易模块系统。 就像"店长后台 + 前台操作台"组合。
🌸第 82 页:闭包的延伸------封装与构造函数结合
继续升级版,讲到闭包 + 构造函数结合。
📘 示例:
ini
function Factory() {
var secretRecipe = '配方保密 🧋';
this.make = function() {
console.log('开始制作奶茶:' + secretRecipe);
};
}
var shop = new Factory();
shop.make(); // 输出:开始制作奶茶:配方保密 🧋
🍬 含义:
secretRecipe
是"私有变量";make()
是"公开方法";- 但外部无法直接访问
secretRecipe
。
🍰 类比:
顾客能点奶茶(调用 make), 但不知道厨房配方(secretRecipe)~ 闭包帮我们实现了"私有属性" !
☀️第 83 页:类型转换机制概览
终于到了 JS 面试最爱问的陷阱 系列之一: 👉 JavaScript 的类型转换机制
💡 JS 中的类型转换分三类:
类型 | 描述 | 举例 |
---|---|---|
显示转换 | 你手动调用函数转换 | Number('123') , String(10) |
隐式转换 | JS 自动帮你转 | '5' * 2 → 10 |
强制转换 | 一些奇怪运算符强制触发 | '3' - 1 , '3' + 1 |
🍹 奶茶店类比:
显式转换:你主动换奶茶口味; 隐式转换:店员自动帮你加冰(没告诉你); 强制转换:顾客搞混点单,系统硬帮你纠正 😆。
🌷第 84 页:显示转换 (Explicit Conversion)
JS 提供了三个常见的手动转换函数:
方法 | 作用 | 示例 |
---|---|---|
Number() |
转数字 | Number('3') → 3 |
String() |
转字符串 | String(123) → '123' |
Boolean() |
转布尔值 | Boolean('') → false |
📘 示例:Number()
javascript
console.log(Number('123')); // 123
console.log(Number('')); // 0
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number('小可爱')); // NaN
🍬 记忆口诀:
"空变零,真变一,假变零,文字懵(NaN)。"
📘 parseInt()
/ parseFloat()
javascript
parseInt('12.3') // 12
parseFloat('12.3') // 12.3
parseInt('abc') // NaN
🍵 类比:
这俩函数像"扫单机":
parseInt
:看到数字就截下来;parseFloat
:连小数点都识别;- 如果一开头就看不懂,就说"我懵了 NaN"。
🧠第 85 页:String()
与 Boolean()
转换
📘 String()
scss
String(123); // "123"
String(true); // "true"
String(null); // "null"
🍰 类比:
"把所有奶茶都装进同一个标签罐里"------统一变成文字。
📘 Boolean()
scss
Boolean('小可爱'); // true
Boolean(''); // false
Boolean(0); // false
Boolean([]); // true
📦 规则:
值 | 转布尔结果 |
---|---|
0, NaN, '', null, undefined, false |
false |
其他 | true |
🍹 奶茶店类比:
"有内容的杯子就算真,有空杯就是假。" 即便是空数组
[]
,杯子还在,所以true
!
🎀 小可爱总结(第81~85页)
概念 | 含义 | 奶茶店比喻 |
---|---|---|
闭包模块化 | 封装数据与操作 | 后台点单系统 |
构造函数+闭包 | 私有属性实现 | 店长配方保密 |
显式转换 | 手动换类型 | 主动改口味 |
隐式转换 | JS 自动帮转 | 店员帮你加冰 |
Number() | 变数字 | 空=0,真=1,假=0 |
Boolean() | 真假判断 | 空杯是假,装奶茶是真 |
🌈 终极口诀:
"闭包藏秘密,模块保隐私; 类型别混乱,真空要牢记; 手动转最稳,隐式要小心~🍵"
🧋第 86 页:类型转换进阶(隐式转换)
💡 隐式类型转换(Type Coercion)
就是 JavaScript 自动帮你"悄悄换类型" 的机制。
🧠 举个例子:
arduino
console.log(1 + '2'); // "12"
console.log('3' * 2); // 6
console.log(true + 1); // 2
🍬 解释:
'+'
有"拼接字符串"的优先级,所以'1' + 2
变"12"
- 乘号
*
只懂数字 →'3' * 2
自动把'3'
变成 3 true
自动变为 1
🍰 类比:
这就像收银员在算账:
- "字符串"就是文字票据;
- "数字"是金额;
- JS 会自动帮你把票据转成金额去加减(有时很聪明,有时瞎搞 😆)。
🌸第 87 页:隐式转换的规则
JS 自动转换时,会根据运算符种类判断目标类型:
运算符 | 转换方向 |
---|---|
+ |
如果任一操作数是字符串 → 转字符串 |
- , * , / |
全部转数字 |
比较运算(==) | 尝试先转数字再比较 |
📘 示例:
arduino
console.log('5' - 2); // 3
console.log('5' + 2); // "52"
console.log('5' * '2'); // 10
console.log('5' == 5); // true
console.log('5' === 5); // false
💬 一句话总结:
"== 会自动帮你转换类型,=== 不会。"
🍹 奶茶店比喻:
==:不挑顾客,身份证号或昵称都行(只要内容相同) ===:严格核验身份证号、名字、出生日期都得一样。
☀️第 88 页:自动类型转换的细节陷阱
JS 的隐式转换有几条"神奇规则"🤯:
值 | 转数字 Number | 转字符串 String | 转布尔 Boolean |
---|---|---|---|
undefined |
NaN | "undefined" | false |
null |
0 | "null" | false |
true / false |
1 / 0 | "true"/"false" | true / false |
[] |
0 | "" | true |
{} |
NaN | "[object Object]" | true |
📘 示例:
ini
console.log([] == 0); // true
console.log([] == ![]); // true (面试陷阱题)
💬 解释: 1️⃣ [] == 0
→ []
转成 0
,结果相等 2️⃣ ![]
先转布尔(false
),再转数字(0
) 所以变成 0 == 0
→ true
🍰 类比:
空杯子看起来没内容(被当成 0), "没奶茶"(false)也当作 0 → 所以两边居然相等😅。
🧃第 89 页:浅拷贝 vs 深拷贝(对象复制)
这一页进入对象复制的考点。
💡 定义
类型 | 说明 | 类比 |
---|---|---|
浅拷贝 | 只复制第一层引用 | 拷贝了奶茶包装,但里面的珍珠还是同一碗 |
深拷贝 | 连内部内容都复制 | 整杯奶茶和配料都重做一份 |
📘 示例:浅拷贝
ini
const obj = { name: '奶茶', info: { taste: '草莓' } };
const copy = Object.assign({}, obj);
copy.info.taste = '波霸';
console.log(obj.info.taste); // "波霸"
💬 为什么? Object.assign()
只复制第一层, 内部的 info
仍是同一个引用地址。
🍹 类比:
拷贝了奶茶标签,但配料桶共用 → 改一边另一边也变!
🌷第 90 页:实现深拷贝的方法
✅ 1. JSON 方法(简易版)
ini
const obj = { name: '奶茶', info: { taste: '草莓' } };
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.info.taste = '波霸';
console.log(obj.info.taste); // "草莓"
🍰 原理: JSON.stringify()
把对象转成字符串; JSON.parse()
再生成一个全新对象。
⚠️ 缺点:
- 无法复制函数;
- 无法处理循环引用;
- 丢失
undefined
。
✅ 2. 递归实现(进阶)
ini
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
result[key] = deepClone(obj[key]);
}
return result;
}
💬 每次遇到对象就递归复制一份,直到最深层都分开。
🍹 奶茶店比喻:
深拷贝 = 从奶茶到珍珠,从珍珠到糖浆都重新制作一份, 两杯完全独立,互不影响。
🎀 小可爱总结(第86~90页)
知识点 | 含义 | 奶茶店比喻 |
---|---|---|
隐式转换 | JS 自动帮你换类型 | 店员偷偷帮你加料 |
== vs === | 前者宽松,后者严格 | 一个看脸,一个查身份证 |
空数组转数字 | [] -> 0 |
空杯子当 0 |
浅拷贝 | 复制标签不复制配料 | 改一边另一边变 |
深拷贝 | 整杯重做 | 互不影响 |
🌈 可爱口诀:
"加减要小心,隐式变类型; == 不可靠,=== 才安心; 拷贝要彻底,奶茶要分离~🧋"
🧋第 91 页:数组拷贝与合并方法
我们先来看最简单的------数组的复制与合并。
📘 slice()
sql
const arr = ['奶茶', '可可', '绿茶'];
const copy = arr.slice();
copy[0] = '草莓奶茶';
console.log(arr); // ['奶茶', '可可', '绿茶']
console.log(copy); // ['草莓奶茶', '可可', '绿茶']
💬 含义: slice()
会返回一个新数组 ,是 浅拷贝。 它不会影响原数组的第一层内容。
🍹 类比:
你复制了奶茶菜单 📋,自己在副本上改成草莓奶茶, 原店菜单没动。
📘 concat()
ini
const arr = ['奶茶'];
const arr2 = ['抹茶'];
const merged = arr.concat(arr2);
console.log(merged); // ['奶茶', '抹茶']
💬 含义: concat()
把多个数组拼接起来,同样返回一个新数组(浅拷贝)。
🍰 类比:
这就像把"奶茶店菜单 + 咖啡厅菜单"合并成一本新菜单📖。
🌸第 92 页:多层结构的陷阱(浅拷贝问题)
📘 示例:
ini
const arr = [{ name: '奶茶' }, { name: '绿茶' }];
const copy = arr.slice();
copy[0].name = '草莓奶茶';
console.log(arr[0].name); // '草莓奶茶'
💬 为什么改动了原数组? 因为 浅拷贝只复制第一层引用 , 对象 { name: '奶茶' }
在原数组和新数组中是"同一个地址"!
🍵 奶茶店类比:
你复制了菜单,但里面的"原料仓"还是同一个, 改掉一种配方,两个菜单都显示变了~😂
☀️第 93 页:深拷贝(Deep Clone)实现方法
现在来看看让"菜单完全独立"的几种深拷贝方法👇
✅ 1. Lodash 的 _.cloneDeep()
ini
const _ = require('lodash');
const obj = { name: '奶茶', detail: { taste: '草莓' } };
const newObj = _.cloneDeep(obj);
newObj.detail.taste = '波霸';
console.log(obj.detail.taste); // 草莓
💡 说明: _.cloneDeep()
会递归复制所有层级内容,是真正的深拷贝。
🍓 类比:
这就像请了一个"奶茶复制机机器人"🤖 每一层原料、每个配方都复制新的,不混用。
✅ 2. jQuery 的 $.extend(true, target, obj)
ini
const obj = { a: 1, b: { c: 2 } };
const copy = $.extend(true, {}, obj);
copy.b.c = 99;
console.log(obj.b.c); // 2
💬 true
参数表示"深度合并(递归复制)"。
🍹 类比:
extend(true, ...)
就像说:"连珍珠桶、糖浆罐都复制一份。"
✅ 3. JSON 方法(简单但有局限)
ini
const obj = { name: '奶茶', taste: { flavor: '草莓' } };
const copy = JSON.parse(JSON.stringify(obj));
copy.taste.flavor = '波霸';
console.log(obj.taste.flavor); // 草莓
💬 优点:简单好用。 ⚠️ 缺点:
- 会丢失函数、undefined;
- 不能处理循环引用。
🍰 类比:
这就像把菜单拍照打印再扫描回来📸, 文本能复制,图片里的隐藏信息(函数)没了。
🧃第 94 页:浅拷贝 vs 深拷贝图示
这页有一张超重要的对比图,我帮你总结一下:
层级 | 浅拷贝 | 深拷贝 |
---|---|---|
一级属性 | ✅ 独立 | ✅ 独立 |
多级对象 | ❌ 共用引用 | ✅ 各自独立 |
📊 图形理解:
浅拷贝:
css原数组 ──→ 对象A 拷贝数组 ──→ 同一个 对象A
深拷贝:
原数组 ──→ 对象A1 拷贝数组 ──→ 对象A2(新建的!)
🍵 奶茶店比喻:
浅拷贝是两个菜单共用一个配料仓。 深拷贝是各自有独立厨房 🔥。
📘 对比示例
ini
const a = { drink: '奶茶', ingredients: { sugar: '三分糖' } };
const b = a; // 引用
const c = JSON.parse(JSON.stringify(a)); // 深拷贝
b.ingredients.sugar = '全糖';
console.log(a.ingredients.sugar); // 全糖(b 改了共用的)
c.ingredients.sugar = '无糖';
console.log(a.ingredients.sugar); // 仍是全糖(c 独立)
🌷第 95 页:闭包缓存机制(函数记忆)
这一页开始讲一个非常有趣的高级话题: 👉 如何用函数 + 闭包实现缓存(Memoization)
💡 核心思想:
当一个函数执行结果"可以被重复利用"时,我们用缓存存下来, 下次调用就不用重新计算。
📘 示例:
ini
function memoizedAdd() {
const cache = {};
return function(x, y) {
const key = x + ',' + y;
if (cache[key]) {
console.log('取缓存~');
return cache[key];
}
const result = x + y;
cache[key] = result;
console.log('计算并存入缓存~');
return result;
};
}
const add = memoizedAdd();
add(1, 2); // 计算并存入缓存~
add(1, 2); // 取缓存~
🍹 奶茶店比喻:
顾客点"草莓奶茶"→厨房先做一杯(存缓存); 下次再有人点同样口味 → 店长直接从冰箱取出成品 🧊!
✅ 优点:
- 节省性能;
- 避免重复计算;
- 减少请求次数。
🍰 实战应用:
- Vue、React 的"计算属性缓存";
- 接口数据缓存;
- 图像处理或复杂计算缓存。
🎀 小可爱总结(第91~95页)
知识点 | 含义 | 奶茶店比喻 |
---|---|---|
slice / concat | 浅拷贝数组 | 复制菜单但共用原料 |
浅拷贝问题 | 多层结构共用地址 | 改配料两边都变 |
深拷贝方法 | 全面复制新对象 | 各自独立厨房 |
cloneDeep / JSON | 两种深拷贝常用法 | 机器人复制 / 拍照扫描 |
函数缓存 | 用闭包保存结果 | 奶茶配方复用 |
🌈 可爱口诀:
"slice 拷壳不拷芯, concat 拼接也浅心; cloneDeep 真香机, 缓存闭包省电力~💡"
🧋第 96 页:函数缓存(Function Memoization)
💡 一、什么是函数缓存?
函数缓存就是------把函数的计算结果"记下来" , 下次遇到相同输入时直接取结果,不再重新算。
🍹 类比:
就像奶茶店老板有"老顾客卡", 你上次点"波霸奶茶三分糖",系统记住了,下次直接调出那杯配方 ✅。
📘 示例:
javascript
function add(x, y) {
return x + y;
}
console.log(add(2, 3)); // 每次都算一次
如果要加缓存👇:
ini
function memoAdd() {
const cache = {};
return function(x, y) {
const key = `${x},${y}`;
if (cache[key]) {
console.log('从缓存拿结果');
return cache[key];
}
console.log('计算并存入缓存');
const result = x + y;
cache[key] = result;
return result;
};
}
const add = memoAdd();
add(2, 3); // 计算并存入缓存
add(2, 3); // 从缓存拿结果
🧠 小总结:
- 第一次计算 → 存缓存;
- 第二次相同输入 → 直接取缓存;
- 减少性能浪费 💪。
🍰 奶茶店版:
顾客第一次点"波霸三分糖", 厨房调一杯 → 存冰箱; 第二次同样的单 → 直接从冰箱拿 🧊。
🌸第 97 页:函数缓存的多种写法
✅ 1️⃣ 闭包写法
利用闭包保存 cache
:
ini
function memo(fn) {
const cache = {};
return function(...args) {
const key = args.join(',');
if (cache[key]) {
console.log('取缓存');
return cache[key];
}
console.log('重新计算');
const result = fn(...args);
cache[key] = result;
return result;
};
}
function add(a, b) { return a + b; }
const cachedAdd = memo(add);
cachedAdd(1, 2); // 重新计算
cachedAdd(1, 2); // 取缓存
✅ 2️⃣ Map结构缓存(性能更好)
vbnet
function memoMap(fn) {
const cache = new Map();
return function(...args) {
const key = args.toString();
if (cache.has(key)) {
return cache.get(key);
}
const res = fn(...args);
cache.set(key, res);
return res;
};
}
💡 Map
更快、更高效,适合缓存大量数据。
🍵 奶茶店类比:
Map
就像"带条码的冰箱"📦, 拿饮品更快更精确,不会重复查找。
☀️第 98 页:函数缓存的实际应用
函数缓存常用于:
- 重复运算(比如斐波那契数列);
- 接口请求缓存;
- 搜索结果缓存。
📘 示例:斐波那契(递归优化)
scss
function fib(n, cache = {}) {
if (n <= 1) return n;
if (cache[n]) return cache[n];
cache[n] = fib(n - 1, cache) + fib(n - 2, cache);
return cache[n];
}
console.log(fib(10)); // 超快!
💬 解释:
递归本来会重复算很多次。 加缓存后,计算过的结果直接用,速度起飞 🚀!
🍰 奶茶店比喻:
你要做"波霸 + 芋圆 + 三分糖"的奶茶, 第一次调出来存档。下次再点,就直接复制 ✅。
🌷第 99 页:字符串操作方法大全(开胃小甜点 🍬)
💡 字符串的三类操作:
类型 | 举例 |
---|---|
操作方法 | 拼接、截取、替换 |
结构方法 | 查找、判断 |
转换方法 | 大小写、数组互转 |
📘 1️⃣ 操作方法 - concat()
ini
let str1 = '奶茶';
let str2 = '三分糖';
console.log(str1.concat(' - ', str2)); // 奶茶 - 三分糖
🍹 类比:
concat 就像"拼接标签机"------ 把两个标签贴成一个新标题。
📘 2️⃣ slice()
ini
let str = '草莓波霸奶茶';
console.log(str.slice(0, 2)); // 草莓
🍓 说明:
截取从 0 到 2 的字符(不包含 2)。
🍰 奶茶店版:
这就像把"草莓波霸奶茶"只拿出"草莓"部分尝尝味道。
📘 3️⃣ substring()
vs substr()
ini
let str = '珍珠奶茶';
console.log(str.substring(0, 2)); // 珍珠
console.log(str.substr(0, 2)); // 珍珠
⚠️ 区别:
substring(start, end)
:截到 end 前一位;substr(start, length)
:截 length 个字符。
🍬 类比:
substring:说"从第几层到第几层" substr:说"从这层开始取几层" 都能把奶茶"截成小份"😆。
☕️第 100 页:字符串更多操作!
📘 4️⃣ indexOf()
/ includes()
javascript
let str = '波霸奶茶';
console.log(str.indexOf('奶')); // 2
console.log(str.includes('茶')); // true
💬 含义:
indexOf
找位置;includes
判断是否存在。
🍹 类比:
"菜单上第几行是奶?" "这杯奶茶里有没有加茶?"
📘 5️⃣ replace()
javascript
let str = '三分糖奶茶';
console.log(str.replace('三分', '半糖')); // 半糖奶茶
💡 替换匹配到的内容。
🍰 奶茶店比喻:
把"订单上的三分糖"换成"半糖"~ 就是
replace
在干的事。
📘 6️⃣ split()
perl
let str = '奶茶,咖啡,果汁';
console.log(str.split(',')); // ['奶茶', '咖啡', '果汁']
💡 按分隔符拆分成数组。
🍓 奶茶店版:
这就像"多杯订单"自动拆分成三杯单独制作的单子。
🎀 小可爱总结(第96~100页)
知识点 | 含义 | 奶茶店比喻 |
---|---|---|
函数缓存 | 保存计算结果 | 老顾客卡 / 冰箱取成品 |
闭包缓存 | 用函数内部变量记住历史 | 店长小本本记菜单 |
Map 缓存 | 高效查找 | 条码冰箱系统 |
字符串 concat | 拼接文字 | 拼标签机 |
slice / substr | 截取文字 | 分杯、半份奶茶 |
replace | 替换文字 | 改订单口味 |
split | 拆分文字 | 拆多杯订单 |
🌈 可爱口诀:
"缓存记结果,字符串真灵巧; 拼接要 concat,截取分杯妙; 替换换口味,拆单 split 到~🧋"
🧋第 101 页:字符串的进阶方法(继续甜品时间)
1️⃣ trim()
/ trimStart()
/ trimEnd()
去掉字符串两端或一端的空格。
javascript
let name = ' 奶茶 ';
console.log(name.trim()); // '奶茶'
console.log(name.trimStart()); // '奶茶 '
console.log(name.trimEnd()); // ' 奶茶'
🍬 记忆法: 就像擦桌子,"trim" 就是擦干净两边的奶茶渍 。 trimStart()
擦左边,trimEnd()
擦右边。✨
2️⃣ repeat()
重复字符串。
arduino
let word = '波霸';
console.log(word.repeat(3)); // 波霸波霸波霸
🍹 类比: 顾客喊"再来三杯!"------服务员复制三份相同订单。
3️⃣ padStart()
/ padEnd()
在字符串的开头或结尾补全。
javascript
let order = '7';
console.log(order.padStart(3, '0')); // '007'
console.log(order.padEnd(5, '*')); // '7****'
🍰 比喻: 就像订单号补零或打星号的系统~让它更整齐漂亮。
4️⃣ toLowerCase()
/ toUpperCase()
全部变小写 / 全部变大写。
ini
let name = 'MilkTea';
console.log(name.toLowerCase()); // milktea
console.log(name.toUpperCase()); // MILKTEA
☕ 比喻: 就像配料表统一格式:
- 小写 = 店员语气柔和
- 大写 = 店长喊话模式("全糖加珍珠!"🤣)
🌸第 102 页:查找类方法
1️⃣ indexOf()
/ lastIndexOf()
找字符在字符串中的位置。
javascript
let drink = '草莓奶茶';
console.log(drink.indexOf('茶')); // 3
console.log(drink.lastIndexOf('莓')); // 1
💡 就像菜单上"第几个字写了茶?" lastIndexOf
是从右边开始找。
2️⃣ includes()
判断是否包含。
ini
let str = '奶茶波霸';
console.log(str.includes('奶')); // true
🍓 类比: 就像问"这杯奶茶里有加奶吗?" → 有就返回 true
。
3️⃣ startsWith()
/ endsWith()
检查字符串是否以某部分开头/结尾。
javascript
let name = '珍珠奶茶';
console.log(name.startsWith('珍珠')); // true
console.log(name.endsWith('奶茶')); // true
🍵 类比:
"这杯奶茶是不是以珍珠打底?" "结尾是不是奶茶味?"
☀️第 103 页:转换类方法
1️⃣ split()
按分隔符拆成数组。
perl
let drinks = '奶茶,咖啡,果汁';
console.log(drinks.split(',')); // ['奶茶','咖啡','果汁']
💡 比喻:
一张总订单分成三张单子,各自制作。
2️⃣ search()
查找匹配的位置(可配合正则)。
arduino
let text = '波霸奶茶';
console.log(text.search('奶茶')); // 2
📦 类比:
就像系统里搜索"奶茶"关键字,告诉你从第几个字开始。
3️⃣ replace()
(加强版)
javascript
let order = '三分糖奶茶';
console.log(order.replace('三分', '半糖')); // 半糖奶茶
🍬 类比:
顾客改口味:"三分糖换成半糖!"
4️⃣ match()
用正则匹配多个。
javascript
let msg = '奶茶123波霸456';
console.log(msg.match(/\d+/g)); // ['123','456']
💬 类比:
就像在菜单里找出所有数字配料编号。
🌷第 104 页:数组的常用方法(重头戏来啦~)
这页开始讲数组(Array), 也就是"多杯奶茶订单集合"。
一、操作类(增删改)
push()
/ pop()
push()
:尾部加一个;pop()
:尾部删一个。
css
let drinks = ['奶茶'];
drinks.push('绿茶'); // ['奶茶','绿茶']
drinks.pop(); // ['奶茶']
🍵 类比:
push = 新订单加入队列 pop = 最后一杯做完送走 🚗
shift()
/ unshift()
shift()
:从头删;unshift()
:从头加。
css
let drinks = ['奶茶','咖啡'];
drinks.shift(); // ['咖啡']
drinks.unshift('果汁'); // ['果汁','咖啡']
🍓 比喻:
shift = 把最早的订单交给顾客 unshift = 新订单插队到最前面!
☕第 105 页:数组遍历方法(逻辑思维区)
1️⃣ forEach()
遍历数组,每个元素都执行一次。
javascript
['奶茶','抹茶','咖啡'].forEach(item => console.log(item));
🍰 类比:
店员一杯一杯确认:"奶茶好了~"、"抹茶好了~"......
2️⃣ map()
生成一个新数组。
ini
let prices = [10, 15, 20];
let newPrices = prices.map(p => p + 5);
console.log(newPrices); // [15, 20, 25]
💡 类比:
把每杯奶茶都加价 5 元,生成新的菜单副本。
3️⃣ filter()
筛选符合条件的。
ini
let drinks = [5, 15, 25];
let result = drinks.filter(p => p > 10);
console.log(result); // [15, 25]
🍵 类比:
从全部订单中筛出"单价大于10元"的奶茶。
4️⃣ reduce()
从左到右累计运算。
ini
let prices = [10, 15, 25];
let total = prices.reduce((sum, p) => sum + p, 0);
console.log(total); // 50
💬 类比:
结账计算总价 💰
🎀 小可爱总结(第101~105页)
分类 | 方法 | 奶茶店记忆 |
---|---|---|
去空格 | trim | 擦掉两边奶渍 |
重复字符串 | repeat | 多杯复制 |
补位 | padStart / padEnd | 补零打星号 |
查找 | indexOf / includes | 看菜单有没有 |
拆分 | split | 拆多杯订单 |
替换 | replace | 改糖度口味 |
添加 | push / unshift | 新订单加入队列 |
删除 | pop / shift | 订单完成送出 |
遍历 | forEach | 逐杯确认 |
映射 | map | 新菜单价表 |
筛选 | filter | 选贵奶茶 |
汇总 | reduce | 结账统计 |
🌈 可爱口诀总结:
"trim 去渍,split 拆杯; push 加单,pop 送杯; map 改价,reduce 结尾; filter 挑单最美味~🧋✨"