框架会过时,语法会演进,但这些底层机制不会变。搞懂它们,你看任何 JS 代码都不会慌。
一、值与引用——一切 Bug 的源头
JavaScript 的数据类型分两大阵营,它们在赋值时的行为截然不同。
原始类型:独立副本
string、number、boolean、null、undefined、bigint、symbol
let a = 10;
let b = a;
b = 99;
console.log(a); // 10 —— b 的修改跟 a 无关赋值时复制的是值本身。两个变量之后完全独立。
引用类型:共享地址
object、array、function
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4] —— 被影响了赋值时复制的是内存地址(引用)。两个变量指向同一块数据,任何一方修改都会互相影响。
浅拷贝的陷阱
展开运算符 {...obj} 和 [...arr] 只复制一层:
const original = { name: "doc", tags: ["pdf", "report"] };
const copy = { ...original };
copy.name = "new"; // ✅ 第一层原始类型,独立
copy.tags.push("urgent"); // ❌ 第一层引用类型,仍然共享
console.log(original.tags); // ["pdf", "report", "urgent"]规则:浅拷贝后,第一层原始类型互不影响,第一层引用类型仍然共享。
需要完全独立的副本时:
const deep = structuredClone(original); // 原生深拷贝,推荐二、作用域与闭包——理解 JS 如何“记忆”
作用域决定变量的可见范围
function outer() {
let x = 1; // 函数作用域
if (true) {
let y = 2; // 块级作用域:只在 {} 内可见
var z = 3; // var 是函数作用域:整个 outer 都可见
}
console.log(x); // 1 ✅
console.log(z); // 3 ✅ var 不受 {} 限制
console.log(y); // ReferenceError ❌
}实际开发只用 let / const,但你得知道 var 的行为——面试常考。
闭包:函数记住出生时的环境
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3createCounter 执行完了,count 本该被回收。但返回的函数引用了它,所以引擎让 count 活着——这就是闭包。
闭包本质上是:函数 + 它诞生时能访问到的所有变量,打包在一起形成的封闭环境。
闭包经典陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3
// var 是函数作用域,三个回调共享同一个 i,循环结束时 i = 3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2
// let 是块级作用域,每次循环创建独立的 i三、对象——JavaScript 的万物基石
核心操作
const doc = { id: 1, title: "设计文档", status: "done" };
doc.title; // 读取
doc.pageCount = 42; // 添加
delete doc.pageCount; // 删除
"title" in doc; // 检查 key 是否存在解构——快速提取
const { title, status } = doc; // 提取
const { title: docTitle } = doc; // 重命名
const { priority = "low" } = doc; // 默认值展开与合并
// 浅拷贝 + 覆盖
const updated = { ...doc, status: "failed" };
// 合并多个对象(后者覆盖前者)
const config = { ...defaults, ...userPrefs };遍历
Object.keys(doc); // ["id", "title", "status"]
Object.values(doc); // [1, "设计文档", "done"]
Object.entries(doc); // [["id", 1], ["title", "设计文档"], ...]四、数组方法——数据操作的核心武器
不改变原数组的方法(优先使用)
const docs = [
{ id: 1, title: "需求文档", status: "done" },
{ id: 2, title: "API 设计", status: "processing" },
{ id: 3, title: "测试报告", status: "failed" },
];
// 筛选
docs.filter(d => d.status === "done");
// 变换
docs.map(d => d.title);
// 查找单个
docs.find(d => d.id === 2);
// 判断
docs.some(d => d.status === "failed"); // 至少一个满足?
docs.every(d => d.status === "done"); // 全部满足?
// 累积
docs.reduce((acc, d) => {
acc[d.status] = (acc[d.status] || 0) + 1;
return acc;
}, {});
// { done: 1, processing: 1, failed: 1 }链式调用
const result = docs
.filter(d => d.status === "done")
.map(d => d.title)
.sort();不改变原数组的增删模式
// 增加:展开 + 新元素
const added = [...docs, newDoc];
// 删除:filter 排除
const removed = docs.filter(d => d.id !== targetId);
// 修改:map 替换
const updated = docs.map(d => d.id === targetId ? { ...d, status: "done" } : d);这三个模式在 React 中是唯一正确的状态更新方式,现在记住,后面不用再学。
五、错误处理——让程序优雅地失败
try / catch
try {
const data = JSON.parse(input);
processData(data);
} catch (error) {
// error.message 包含具体原因
showErrorToUser("数据格式错误:" + error.message);
}主动抛出
function uploadFile(file) {
if (!file) throw new Error("文件不能为空");
if (file.size > 10 * 1024 * 1024) throw new Error("文件不能超过 10MB");
// ...
}原则
- 在能处理的地方 catch,不能处理就让它往上抛
- 永远不要写空 catch——
catch {}会吞掉错误,让 bug 无迹可寻 - 对用户给出有意义的提示,对开发者记录完整的 error 对象
六、事件冒泡——理解浏览器事件的传递路径
机制
点击一个按钮时,事件从目标元素逐层向上传递到 document:
button → li → ul → div → body → document每一层如果绑了同类型事件,都会被触发。
事件委托:利用冒泡优化性能
// 不好:100 个按钮绑 100 个监听器
items.forEach(item => {
item.btn.addEventListener("click", () => deleteDoc(item.id));
});
// 好:父元素绑 1 个,利用冒泡 + e.target 判断来源
list.addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) {
deleteDoc(Number(e.target.dataset.id));
}
});事件委托的优势:
- 监听器数量从 N 降到 1
- 后续动态添加的子元素自动生效,不用重新绑定
阻止冒泡
btn.addEventListener("click", (e) => {
e.stopPropagation(); // 阻止继续向上传递
});附:一个容易混淆的点
传函数 vs 调用函数
// ✅ 把函数作为参数传递(一张纸条,等事件发生时执行)
button.addEventListener("click", handleClick);
button.addEventListener("click", () => deleteDoc(id));
// ❌ 当场调用函数,把返回值传给 addEventListener
button.addEventListener("click", handleClick()); // 立即执行了!
button.addEventListener("click", deleteDoc(id)); // 立即执行了!fn 是函数本身,fn() 是执行函数并拿到返回值。addEventListener 需要的是前者。
当你需要给回调传参数时,用箭头函数包一层——它创建了一个新函数,把真正的调用推迟到事件触发时:
() => deleteDoc(id)
// 等价于
function() { deleteDoc(id); }总结
| 概念 | 一句话 |
|---|---|
| 值与引用 | 原始类型复制值,引用类型复制地址 |
| 浅拷贝 | 只独立第一层,嵌套对象仍共享 |
| 作用域 | let/const 块级,var 函数级 |
| 闭包 | 函数记住诞生时的变量环境 |
| 数组方法 | filter 筛选、map 变换、find 查找、reduce 累积 |
| 事件冒泡 | 从内到外逐层传递,可用于事件委托 |
这六个概念不是“学完就忘”的东西——它们是你后续写 React、处理异步、管理状态时每天都在用的底层机制。理解到位了,框架只是 API 的区别。

没有回应