如果说前5天是把 JavaScript 的基本语法装进脑子,那 day6-10 就是逼迫你把这些语法真正用起来的分水岭。很多初学者都会有这种感觉:变量、循环、函数声明都看懂了,但一到写 DOM 交互、处理接口返回,就完全不知道怎么下手。标题里说的“核心知识体系总结 day6-10”,其实就是 JavaScript 从“语法认知”迈向“工程实践”的过渡阶段,覆盖了作用域、闭包、高阶函数、DOM 事件、异步请求这些以后写任何项目都躲不开的内容。
这篇总结适合两类人:一类是刚学完基础语法、想找一个系统路线继续往下走的新手;另一类是已经写过一些页面,但总觉得自己对函数、事件、异步的底层逻辑不够清楚,想回头补课的同学。文章不会从零重复讲数据类型和循环,而是把 day6-10 之间最关键的知识点拆开,结合真实代码和踩坑经历来讲,让你学完能直接往项目里用。
1. day6-10 整体知识框架:这个阶段到底在解决什么问题
1.1 为什么 day6-10 是入门阶段的分水岭
day1-5 的内容基本可以概括为“认得字、会造句”:变量、数据类型、运算符、条件分支、循环。这些知识的特点是单点独立,就算不太理解背后的执行逻辑,也能靠模板写法应付过去。但从 day6 开始,内容的性质变了,所有知识点开始交叉:函数要在作用域中执行,数组方法其实是函数式思维,DOM 事件绑定本质是回调函数的应用,异步请求又要依赖回调或者 Promise。任何一个环节没打通,后面都会越学越吃力。
我在带新人时发现一个典型现象:很多人能背出闭包的定义,却解释不了为什么循环里用 var 绑定事件点击后打印的全是同一个值;能写出 fetch(...).then(...) 的调用,但报错后不知道从哪里排查。这就是知识碎片化、没有形成体系的表现。day6-10 的规划就是要解决“散装 JavaScript”的问题,把函数、作用域、事件、异步串成一条线。
1.2 常见课程里 6-10 天的内容怎么分布
不同教程的节奏不完全一样,但按我多年的经验和主流知识体系来看,day6-10 通常是这样划分的,也推荐你参考这个节奏来复习:
| 时间 | 核心主题 | 需要掌握到什么程度 |
|---|---|---|
| day6 | 作用域、闭包、变量提升 | 能解释闭包的形成原理,能写出简单闭包并知道实际用途 |
| day7 | 函数进阶、箭头函数、柯里化 | 能区分普通函数和箭头函数的 this 规则,能自己封装小工具函数 |
| day8 | 数组高阶方法、对象操作 | 能熟练使用 map/filter/reduce/find 替代循环,理解不可变数据 |
| day9 | DOM 增删改查、事件体系 | 能独立实现选项卡、弹窗、列表渲染等常见交互 |
| day10 | 异步编程、fetch API、模块化 | 能正确请求接口并渲染数据,能处理 loading 和错误状态 |
这个安排不是拍脑袋定的。它遵循了一个重要的认知逻辑:先用 day6-7 搞定 JavaScript 最难理解的函数和作用域,因为所有事件回调、异步回调都是函数的另一种用法;再用 day8 的数组方法和对象操作解决“拿到数据怎么处理”的问题;day9 把处理好的数据交给 DOM 渲染,形成完整闭环;最后 day10 的异步请求则是把前面所有能力综合调用起来。换句话说,前 5 天是点,6-10 天是线,从这套体系之后,你就能自己画出简单的面。
1.3 这个阶段最容易被忽略的一条主线:动态与静态的分界
我还想强调一个视角:day6-10 教会你的本质上是“动态执行”。作用域链决定了代码在运行时去哪找变量,闭包让变量在函数执行完后依然存活,事件系统让同一份代码可以响应不同用户操作,异步让程序不用等网络请求完成就能继续往下跑。如果你学的时候只去背 API,很难建立体系感;但如果你带着“程序是怎么在运行时一步步动态执行”的疑问去学,很多别扭的地方都会自然解开。
所以接下来我不会只罗列知识点,而是以“动态执行”为暗线,把每个知识模块掰开讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 函数、作用域与闭包:入门阶段最大的拦路虎
2.1 作用域链的本质是一次变量查找路线
很多人记住一句话:“JavaScript 是词法作用域”,但不知道这句话到底有什么用。我换个说法你就懂了:在 JavaScript 里,函数内部出现一个变量时,引擎不是全局乱找,而是从当前作用域开始,一层一层往外层作用域找,找到就停止,找不到就报 ReferenceError。这种嵌套关系是在代码编写时,也就是词法解析阶段就已经确定的,所以叫词法作用域。
来看一个例子:
javascript复制const globalVar = '全局';
function outer() {
const outerVar = '外层';
function inner() {
const innerVar = '内层';
console.log(innerVar);
console.log(outerVar);
console.log(globalVar);
}
inner();
}
outer();
inner 函数内部访问 outerVar 时,它自己作用域里没有,于是向外跑到 outer 的作用域里找到;访问 globalVar 时,又继续向外跑到全局找到。这个链式查找过程,就是作用域链。
实际开发中理解这个机制有什么用?最大的用处是排查变量污染和命名冲突。如果你在全局声明了大量变量,又在函数里不小心用了同名变量,就可能产生预期之外的覆盖。我见过不少新人因为图省事,把所有临时变量都写在全局,后期定位问题简直灾难。养成“能用局部变量就不用全局变量”的习惯,能帮你躲掉大半诡异的 bug。
2.2 闭包:不是玄学,是作用域链的自然结果
闭包的定义五花八门,有的说法能把人绕晕。我的理解非常朴素:当一个内部函数引用了外部函数的变量,并且这个内部函数被返回到了外部函数之外使用,外部函数的变量就不会被垃圾回收机制清掉,因为还有人在引用它。这个“内部函数 + 被保留的外部变量”的组合,就是闭包。
举个例子:
javascript复制function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
正常情况下 createCounter 执行完,count 就应该被销毁了,但因为内部匿名函数还“记住”了它,count 被保留下来。这个特性最常见的用途就是封装私有变量,让外部不能直接改 count,只能通过返回的函数操作。
闭包在真实项目中其实无处不在,不一定非要手动写一个。事件回调、定时器回调、防抖节流函数、React/Vue 中的很多 hook 逻辑,底层都有闭包的身影。day6 阶段你没必要深挖到闭包内存管理的程度,但至少要做到:看到类似的代码能说清变量为什么被保留,面试里最常见的“点击循环列表项输出索引”问题也能解释得通。
2.3 箭头函数:语法糖背后的 this 规则差异
现在前端项目里箭头函数用得几乎比普通函数还多,很多人图省事看到 function 就改成 =>,结果遇到 this 问题一头雾水。要弄清这个,得先理解普通函数的 this 是“调用时确定”的。
javascript复制const obj = {
name: '博客',
greet: function () {
console.log(this.name);
},
};
obj.greet(); // 博客
greet 被 obj 调用时,this 指向 obj。但如果把函数单独拿出来调用,this 指向就可能变成全局对象或 undefined,具体取决于运行环境,这就是普通函数 this 的“动态性”。
箭头函数就不一样,它本身不创建自己的 this,而是继承定义它时所处作用域的 this。用代码说明:
javascript复制const obj = {
name: '博客',
greet: function () {
const inner = () => {
console.log(this.name);
};
inner();
},
};
obj.greet(); // 博客
inner 是箭头函数,它的 this 是在定义时从 greet 作用域继承的,而 greet 由 obj 调用,this 指向 obj,所以打印正常。如果把 inner 改成普通函数,这里就会打印 undefined。箭头函数的 this 就是“定义时定的,改不了”,所以不能用 call、apply、bind 改变它的指向,同时箭头函数也不能当作构造函数使用。这也是为什么在 setTimeout、事件监听里,箭头函数经常更好用的原因:
javascript复制class Counter {
constructor() {
this.count = 0;
}
start() {
setInterval(() => {
this.count++;
console.log(this.count);
}, 1000);
}
}
如果这里的回调是普通函数,this 就会丢失,用箭头函数则能稳定拿到实例的 this。day7 阶段把这条规则刻进脑子,后面写 Vue 组合式函数、React 组件都会少踩很多坑。
3. 数组高阶方法:用函数式思维处理数据
3.1 map、filter、reduce 的搭配使用
很多初学者处理数组,第一反应就是 for 循环。循环当然没错,但代码一多,可读性和维护性都会下降。day8 阶段的重点是学会用数组自带的高阶方法,把“怎么遍历”交给引擎,把注意力放在“要对数据做什么”上。
几个最常用的方法:
- arr.map(item => 新值):遍历数组并返回一个等长新数组,适合做数据映射。
- arr.filter(item => 条件):遍历数组并筛出符合条件的项,返回新数组。
- arr.find(item => 条件):返回第一个符合条件的元素本身,找不到返回 undefined。
- arr.some(...):只要有满足条件的就返回 true。
- arr.every(...):所有元素都满足才返回 true。
- arr.reduce((累加器, 当前值) => 新累加器, 初始值):灵活做求和、分组、合并等聚合操作。
举个实际一点的场景:从接口拿到一组用户数据,需要筛选出年龄大于 18 的用户,再只保留姓名和年龄,最后按年龄排序。
javascript复制const users = [
{ name: '张三', age: 17 },
{ name: '李四', age: 22 },
{ name: '王五', age: 19 },
];
const result = users
.filter(user => user.age > 18)
.map(user => ({ name: user.name, age: user.age }))
.sort((a, b) => a.age - b.age);
console.log(result);
这种链式写法最大的优势是每一步语义清晰:过滤、映射、排序。相比之下,一次性 for 循环虽然也行,但以后别人看代码得逐行读才能猜出你要干嘛。
3.2 reduce 是数组方法的集大成者
reduce 是很多人的老大难,因为它的参数确实比别的数组方法多一点:第一个参数是回调函数,回调函数接收四个参数,最常用的是前两个,分别是上一个累积值和当前元素;第二个参数是累积值的初始值。我建议新手先记住一个套路:先写初始值,再在回调里写“怎么把当前元素合并进累积值”。
以数组求和为例:
javascript复制const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10
acc 一开始是 0,然后依次加上 cur 的值:1、3、6、10。reduce 也能用来把数组转成对象,实现按属性分组:
javascript复制const orders = [
{ type: '水果', amount: 10 },
{ type: '蔬菜', amount: 6 },
{ type: '水果', amount: 8 },
];
const grouped = orders.reduce((result, order) => {
const { type, amount } = order;
if (!result[type]) {
result[type] = 0;
}
result[type] += amount;
return result;
}, {});
console.log(grouped); // { 水果: 18, 蔬菜: 6 }
3.3 使用数组方法的四个常见误区
我总结了新人最容易犯的四个错误,写在这里帮你避坑。
第一,map 回调里忘记 return。箭头函数如果直接用花括号包起来,就必须手动写 return,否则返回值是 undefined。很多人写成 map(item => { item.name }) 这样,结果得到全是 undefined 的数组。只要你打算返回一个对象,要么用圆括号包住对象字面量,要么显式写 return。
第二,filter 条件写反。filter 的回调返回 true 时保留元素,返回 false 时过滤掉,测试条件时先想清楚“我要保留什么样的人”,而不是“不要什么样的人”。写 ! 的时候尤其注意。
第三,以为 filter、map 会修改原数组。实际上它们都返回新数组,原数组不受影响。这是函数式编程里“不可变数据”的思想,但有的新人没意识到,直接写 arr.filter(...) 却不接收返回值,导致筛选结果丢失。
第四,reduce 忘记给初始值。虽然数组有元素时 reduce 可以不给初始值,第一个元素会直接作为 acc,但行为容易意外。比如对空数组调用 reduce 不给初始值,直接报 TypeError。所以除非你有十足把握,否则都建议传一个明确的初始值。
4. DOM 操作与事件体系:从“数据在手”到“页面交互”
4.1 DOM 查询和属性修改的基本套路
到了 day9,你已经能用数组方法处理好数据了,下一步就是把这些数据渲染到页面上,并且让用户能通过点击、输入等操作触发逻辑。DOM(文档对象模型)就是 JavaScript 和页面元素之间的桥梁。
最常见的 DOM 操作无非以下几类:
- 查找元素:document.querySelector('.class')、querySelectorAll、getElementById。
- 修改内容:element.textContent = 'xxx',比 innerHTML 安全,不会把字符串当 HTML 解析。
- 修改样式:element.style.color = 'red',涉及多个样式时会用 classList 切换 class 而不是逐个改 style。
- 创建和插入节点:document.createElement('div')、appendChild、insertBefore、removeChild。
一个简单列表渲染可以这样写:
javascript复制const listData = ['JavaScript', 'HTML', 'CSS'];
const ul = document.querySelector('#list');
listData.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
ul.appendChild(li);
});
这里没有用 innerHTML 拼字符串,而是用 createElement 逐个创建。数据量不大时两种都行,但用 createElement 能避免拼字符串时忘记转义导致的问题,尤其是列表内容来自用户输入时,innerHTML 很容易变成 XSS 攻击入口。哪怕只是做一个练习项目,我也建议你从一开始就养成“能用 textContent/DOM API 就不用 innerHTML”的习惯。
4.2 事件绑定与事件委托:别再给每个子元素单独绑事件
事件是 JavaScript 交互的根基。入门阶段你至少需要分清 addEventListener 和直接写在 HTML 上的 onclick 的区别,推荐统一用 addEventListener,它允许同一个元素绑定多个事件处理函数,也方便后面移除监听。
看一个常见需求:点击多个列表项时打印它对应的索引。
javascript复制const items = document.querySelectorAll('.item');
items.forEach((item, index) => {
item.addEventListener('click', () => {
console.log(index);
});
});
这段代码本身没问题,但如果列表是动态渲染的,数据一变,新插入的 li 就没有绑定了。怎么解决?用事件委托。事件的冒泡机制保证点击子元素时事件会一层层传到父容器,所以在父容器上监听一次,再通过 event.target 判断点击的是不是目标元素,就能覆盖所有子元素,包括未来新增的。
javascript复制const ul = document.querySelector('#list');
ul.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
console.log(event.target.textContent);
}
});
4.3 事件循环:setTimeout(fn, 0) 为什么不是立刻执行
你大概率碰到过这样的情况:写了一段代码,先用 setTimeout 延迟 0 毫秒执行回调,却发现回调排在当前代码的后面,而不是最开始。这是因为 JavaScript 是单线程的,同步代码执行完成后,才会去任务队列里拿异步回调执行。
这个机制入门阶段不需要研究得太深,但至少要建立两个认知。
第一,不要把“延迟 0 秒”理解成“立刻执行”,它只是把任务排到了当前同步任务结束后。第二,事件回调本身也遵循这个机制。当用户点击页面时,浏览器收到事件,不会中断当前正在执行的 JavaScript 代码,而是把回调放到任务队列,等当前任务执行完了再处理。
如果你在开发 macos 或浏览器环境中的应用时遇到点击没有反应,先打开控制台看有没有脚本报错,很多时候不是事件没绑上,而是代码执行到一半就中断了。关于环境配置和报错,后面我会专门讲。
5. 异步请求与 fetch API:入门到实战的最后一步
5.1 fetch 的基本调用链
day10 的重点是请求接口。现代浏览器里最原生的异步请求方式就是 fetch,它是基于 Promise 设计的,比老一代的 XMLHttpRequest 清爽得多。
最基本的 GET 请求写法如下:
javascript复制fetch('https://api.example.com/posts')
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('请求出错', error);
});
这里有个新手必踩的坑:fetch 只有在网络层真正失败(比如断网、域名不存在)时才会走到 catch,而服务器返回 404、500 这类 HTTP 错误状态时,fetch 并不会把 Promise 变为 rejected,它仍然会 resolve,只是 response.ok 是 false。所以你需要手动判断:
javascript复制fetch('https://api.example.com/posts')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('请求出错', error);
});
5.2 async/await 让代码更接近同步阅读习惯
Promise 的 then 链虽然有改进,但多层嵌套依旧可读性不佳。async/await 是 Promise 之上的语法糖,让异步代码看起来像同步代码:
javascript复制async function loadPosts() {
try {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('请求出错', error);
}
}
使用 async/await 时有几个细节要注意。await 只能在 async 函数里使用,所以最外面需要一个 async 包裹。错误处理推荐用 try/catch,否则一个 await 失败会导致整个函数中断。
在我实际带新人的过程里,发现很多人把 fetch 的 response.json() 也搞混,以为 fetch 返回的就是数据。其实第一个 then 拿到的 response 只是一个响应对象,必须调用 json()、text()、blob() 等方法才能把响应体解析成可用格式。
5.3 fetch 的完整参数:POST、请求头和 body 的写法
GET 请求直接拼接 URL 即可,但实际开发中更常用的是 POST 接口。fetch 的第二个参数就是配置对象,最常用的几个字段如下:
- method:请求方法,字符串,比如 'POST'。
- headers:请求头对象,通常设置 Content-Type 为 'application/json',告诉服务器你发的是 JSON。
- body:请求体,POST/PUT 等请求用 JSON.stringify(数据) 转成字符串发送;如果是 FormData,可以不用手动设置 Content-Type。
javascript复制async function createPost(title, content) {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, content }),
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
这里容易踩的坑是 body 忘了序列化,直接把对象传进去,会得到类似 “[object Object]” 的字符串,服务端解析不出来。另一个常见问题是设置了 Content-Type,但实际数据是 FormData 格式,导致请求被服务端拒绝。写请求时先把“我在发什么格式的数据”想清楚。
5.4 跨域、运行时错误和环境配置的排查思路
很多初学者请求写好后,控制台报错却是从“javascript运行时报错”开始的一大串红字,看不懂就抓瞎。我这里给一个按优先级排查的思路,按步骤走能省很多时间。
第一步,看报错类型和行号。打开浏览器开发者工具的 Console 页,红色报错信息里通常会标出是语法错误(SyntaxError)、类型错误(TypeError)、引用错误(ReferenceError),还带具体的文件名和行号。点进去就能定位到出错代码行,大多数时候就是那一行内部出现的逻辑问题。第二步,如果是网络请求失败,切到 Network 面板,查看请求的状态码。4xx 说明请求本身有问题,比如 404 接口路径不对、401 未登录;5xx 是服务端问题,重点排查后端。第三步,如果状态码是 200 但拿不到数据,大概率是后端返回的 JSON 结构和预期不一致,Console 里打印一下 response.json() 的结果就能确认。
另外聊一个和 Vue + Element Plus 相关的热搜问题:为什么全局自动引入了 Element Plus,使用 ElMessage 却提示未定义。这个问题的核心原因是 ElMessage 这类命令式组件不像普通组件一样可以通过全局注册后在模板里直接用,它需要显式从包里引入:
javascript复制import { ElMessage } from 'element-plus';
如果项目里配了自动导入插件,通常也要额外在引入列表里声明 ElMessage。报错信息提示的就是 JavaScript 在运行时期找不到 ElMessage 这个标识符。
JavaScript 运行时报错里,有一类尤其常见:undefined is not a function,或者 Cannot read properties of undefined(reading xxx)。这类报错的实质是你在一个不存在的对象上调用了方法或读取了属性。遇到它,最有效的办法是往上一层排查:那个变量在前一步为什么是 undefined?八成是接口返回结构变了,或者异步数据还没到达就渲染了。
还有一个经典元素:javascript:void(0)。这是一种古老的阻止页面跳转的写法,常见于 a 标签的 href 属性中。写成 href="javascript:void(0)" 以后,点击链接页面不会发生跳转,click 事件交给 JavaScript 处理。现在更推荐的做法是直接用 button 元素,或者在 click 事件里调用 event.preventDefault(),语义更清晰,也便于无障碍访问。
6. 我从 day6-10 教学和实战中总结的避坑经验
6.1 每天结束前必须完成一次“代码复现”
不是看懂就算学会。我见过太多人看视频时觉得“这也太简单了”,合上电脑后什么也写不出来。day6-10 的知识点和前 5 天不同,它更强调组合应用,所以我的建议是:每天结束前,关掉任何参考文档,只根据当天的主题,独立实现两到三个功能。比如 day7 学完箭头函数,就写一个用箭头函数实现的简易防抖工具;day8 学完数组方法,就用 map/filter/reduce 处理三个不同场景的虚拟数据;day9 学完事件,就做一个点击切换 Tab 的页面。能独立写出来,才算真正内化。
6.2 一定要养成看浏览器控制台的习惯
报错不是洪水猛兽,它是 JavaScript 帮你定位问题的工具。很多同学一看到控制台红字就慌,其实大部分入门报错的解法都写在错误信息里了。遇到报错先不要急着改代码,静下来读一遍报错文案,再用我前面说的排除法定位。我自己排查问题时,90% 的工作都在 Console 和 Network 两个面板里完成。你越早习惯这两个面板,后面写任何前端框架的项目都会越顺利。
6.3 作用域、闭包和 this 这“三座大山”,值得多过一遍
很多有一定经验的前端开发者,面试时也会在闭包和 this 上翻车,说明这些知识点确实不是看一次就牢固的。如果你在 day6-10 阶段觉得吃力,完全可以放慢节奏,同一章多看两遍,多写几个例子,甚至故意写错再修正。这比赶进度有意义得多,因为后面学 Vue、React,遇到生命周期、响应式原理、hooks 时,还是绕不开这些基础概念。
6.4 做一个小项目来串起全套知识点
如果时间允许,day10 之后强烈建议做一个小项目。用一个很简单但完整的例子:获取一组用户列表,展示在页面上,支持输入关键词筛选,还可以点击列表项查看详情。这个项目虽然小,却能把数据请求、数组筛选、事件绑定、DOM 渲染全部串起来。做完之后,你会对整个知识体系产生完全不同的理解。
7. 写在最后:学会 JavaScript 最好的方式是动手解决具体问题
做了这么多年前端,我的体会是:JavaScript 入门不是靠“读完”的,而是靠“写完”的。day6-10 的存在意义,就是逼你把前 5 天学到的零散语法放进真实场景里打磨。你可能会觉得函数与闭包抽象、数组方法记混、事件处理和异步请求容易出错,但这些地方恰恰是很多人没能跨过的坎。
按照我的经验,这个阶段最重要的是别求快。每学一个新的方法,就亲手写一个小例子验证它的行为;每遇到一个奇怪的报错,就把它当成一次深入理解 JavaScript 的机会。花在这个阶段的时间不会白费,它决定了你之后学框架能走多稳。
最后分享一个我经常让新人做的练习:不用任何框架,只用一个 HTML 文件加一段原生 JavaScript,实现一个接口请求结果的分页列表。这个练习看起来简单,但能把事件委托、数组 slice、加载状态、异常处理全都牵扯出来。能把这道题完整、健壮地写完,你的 JavaScript 入门之旅就算真正过关了。
