JavaScript 核心基础:你必须真正理解的 6 个概念

框架会过时,语法会演进,但这些底层机制不会变。搞懂值与引用、作用域闭包、对象、数组方法、错误处理、事件冒泡这 6 个概念,你看任何 JS 代码都不会慌。

JavaScript 核心基础博客封面图,初音未来

框架会过时,语法会演进,但这些底层机制不会变。搞懂它们,你看任何 JS 代码都不会慌。

一、值与引用——一切 Bug 的源头

JavaScript 的数据类型分两大阵营,它们在赋值时的行为截然不同

原始类型:独立副本

stringnumberbooleannullundefinedbigintsymbol

let a = 10;
let b = a;
b = 99;
console.log(a); // 10 —— b 的修改跟 a 无关

赋值时复制的是值本身。两个变量之后完全独立。

引用类型:共享地址

objectarrayfunction

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(); // 3

createCounter 执行完了,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");
  // ...
}

原则

  1. 在能处理的地方 catch,不能处理就让它往上抛
  2. 永远不要写空 catch——catch {} 会吞掉错误,让 bug 无迹可寻
  3. 对用户给出有意义的提示,对开发者记录完整的 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 的区别。

没有回应

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注