
这次我们来看一份 JavaScript 基础全套教程。它的定位很明确给零基础或基础不牢的前端入门者用 2 小时左右把 JS 的核心语法、DOM 操作、异步编程和实战项目全部过一遍全程没有多余的铺垫。如果你正准备转前端、复习 JS 基础、或者手头有一套前端八股文但缺少可运行的代码验证这份教程值得直接收藏。对比市面上的 JS 教程这套内容有几个突出的特点一是“全程无废话”每个知识点都直接给定义、给代码、给运行结果二是覆盖完整从变量、数据类型、函数、对象、数组到 DOM、事件、ES6 新语法、Promise 和 async/await 都有三是带实战项目不是只讲语法最后能做出一个可交互的页面效果四是附源码和文档学习过程中可以对照源码自己改、自己跑。本文会按照这套教程的骨架把每个阶段要掌握的知识点、代码示例、验证方式和常见坑完整梳理一遍你可以当作一份带笔记的源码文档来看。这套内容适合这几类读者刚开始学前端、想快速建立 JS 知识框架的人学过 JS 但知识零散、想系统梳理的人准备前端面试、需要快速过一遍基础概念和手写代码的人以及想用最短时间做出一个小 Demo 的实战派学习者。1. 核心能力速览能力项说明教程定位JavaScript 零基础入门到实战2 小时快速过完基础语法与核心应用核心内容变量与数据类型、运算符、流程控制、函数、对象、数组、DOM 操作、事件、ES6 新特性、异步编程、实战项目代码形式每节均有可复制的源码示例附带学习文档运行环境浏览器 DevTools 控制台、本地 HTML 文件、Node.js 环境均可适合人群前端初学者、基础不牢的开发者、面试前复习 JS 的求职者学习方式跟着代码边写边跑建议每节完成后自己改参数验证输出结果一个可交互的实战页面 一套完整的 JS 基础笔记是否需要 GPU/服务器不需要普通电脑本地运行即可是否支持批量任务不涉及模型部署但实战项目可演示数组与 DOM 的批量渲染配套资源源码 文档按教程目录组织可直接对照学习这里先说明一点JavaScript 入门不需要复杂的编译环境。你只需要一个浏览器、一个文本编辑器最多再装一个 Node.js 用来跑后台脚本。所以这套教程的准入门槛很低任何普通笔记本电脑都能完成。2. 适用场景与学习边界2.1 这套教程适合解决什么问题JavaScript 是前端开发的核心语言HTML 负责页面结构CSS 负责样式JavaScript 负责交互行为。如果你只是照着 HTML 标签写静态页面可以用不到 JS但只要涉及点击、输入、动态渲染数据、请求后端接口、轮播图、表单校验、菜单折叠这些交互就必须靠 JavaScript 驱动。这套教程解决的正是“从会写静态页面到能写交互页面”这个关键跨越。学完后你会掌握这些能力用变量和数据类型存储页面状态用函数封装重复逻辑用数组方法批量处理列表数据用 DOM API 动态增删改页面元素用事件监听响应用户操作用 Promise 和 async/await 处理接口请求。2.2 学习边界与侧重点这套教程的目标是 JS 基础不会深入到框架层面。Vue、React、Node.js 服务端开发、工程化构建工具、TypeScript 这些内容不属于本教程覆盖范围。学完这套基础后你再看 Vue 或 React 的官方文档会明显轻松很多因为框架本质上是在 JS 基础之上封装了一套响应式和组件化方案。2.3 版权与合规说明教程附带的源码和文档适合个人学习、笔记整理和面试复习使用。如果你想基于源码做二次发布、商用教学或者打包成自己的付费内容请先确认原作者的授权范围。实战项目中如果使用了第三方图片、字体和接口数据也要保留署名信息。技术学习的同时养成版权意识是前端开发者的基本素养。3. 环境准备与前置条件3.1 操作系统Windows、macOS、Linux 都可以。JavaScript 本身跨平台浏览器环境完全一致Node.js 也有各平台安装包。教程里的代码如果涉及文件路径需要按自己的系统调整斜杠方向。3.2 浏览器与开发者工具推荐使用 Chrome 或 Edge 最新版。打开页面后按 F12 进入 DevTools选择 Console 面板就可以直接输入 JavaScript 代码运行。这是最快的验证方式不需要创建任何文件。3.3 代码编辑器推荐 VS Code免费、插件生态好、前端开发体验最完整。建议安装这几个插件ESLint实时检查 JS 语法错误Prettier - Code formatter统一代码格式Live Server为本地 HTML 文件启动一个热更新服务器JavaScript (ES6) code snippets补全常用语法片段如果不想折腾插件直接用记事本或系统自带的编辑器写.html文件在浏览器里双击打开也可以运行。3.4 Node.js可选但建议装Node.js 主要用途是脱离浏览器跑 JavaScript 脚本以及后续学习 npm 包管理、Vue 和 React 时必须使用。去 Node.js 官网下载 LTS 长期支持版安装完成后打开终端验证node -v npm -v能看到版本号就说明安装成功。教程中的算法练习、异步编程练习都可以先用 Node.js 跑速度更快也更接近工程环境。3.5 目录结构建议建议把教程中的代码按章节分目录保存方便复习和排错。比如js-basic-tutorial/ ├── 01-变量与数据类型/ │ ├── demo.html │ ├── index.js │ └── notes.md ├── 02-运算符与流程控制/ │ ├── demo.html │ └── index.js ├── 03-函数/ ├── 04-数组与对象/ ├── 05-DOM操作/ ├── 06-事件处理/ ├── 07-ES6新特性/ ├── 08-异步编程/ └── 09-实战项目/ └── index.html保持这种“一章节一个文件夹”的习惯后面复习、迁移、面试前翻代码都会非常高效。4. JavaScript 基础语法与核心概念4.1 变量与数据类型JavaScript 中的变量声明有三种方式var、let、const。现在实际开发中默认使用let和constvar主要在老代码和面试题中见到。声明方式作用域是否可以重新赋值是否可以先使用后声明var函数作用域可以可以值为 undefined存在变量提升let块级作用域可以不行暂时性死区const块级作用域不可以不行先看一个完整的变量声明与重新赋值示例let name 前端学习者; name 开发者; // 允许重新赋值 const age 18; // age 20; // 报错Uncaught TypeError: Assignment to constant variable. var oldWay 旧时代写法;JavaScript 中常见的数据类型包括字符串、数字、布尔值、undefined、null、对象、数组、函数等。可以用typeof检查类型let str hello; let num 42; let bool true; let empty null; let notDefined; let obj { name: 张三 }; let arr [1, 2, 3]; console.log(typeof str); // string console.log(typeof num); // number console.log(typeof bool); // boolean console.log(typeof empty); // object这是 JS 的历史遗留特性 console.log(typeof notDefined); // undefined console.log(typeof obj); // object console.log(typeof arr); // object数组本质上是对象这里要注意typeof null返回object这是 JavaScript 早期设计留下的 bug但官方一直没有修复。很多前端面试题会考这一点属于 JS 基础中的基础。4.2 类型转换与隐式类型转换前端开发中最容易出 bug 的地方之一就是类型转换。看一个典型例子let result 5 3; console.log(result); // 53字符串拼接 let result2 5 - 3; console.log(result2); // 2因为减号没有字符串拼接语义 let result3 5 * 3; console.log(result3); // 15加号只要一侧是字符串就会执行拼接而减号、乘号、除号会尽量把字符串转成数字再计算。要避免不确定的类型转换建议用显式转换let str 42; let num Number(str); // 42 let str2 String(num); // 42 let intValue parseInt(42px); // 42 let floatValue parseFloat(3.14em); // 3.144.3 运算符与流程控制流程控制主要分为条件判断if、switch循环for、while、for...of、forEach。let score 75; let level ; if (score 90) { level 优秀; } else if (score 60) { level 及格; } else { level 需要努力; } console.log(level); // 及格循环遍历数组的新旧写法对比const list [HTML, CSS, JavaScript]; // 老式索引循环 for (let i 0; i list.length; i) { console.log(list[i]); } // for...of 遍历值 for (const item of list) { console.log(item); } // forEach 方法 list.forEach((item, index) { console.log(index, item); });这里值得注意const声明在for...of和forEach中是可以正常使用的因为每次迭代都会创建新的绑定不会产生重新赋值冲突。5. 函数与作用域5.1 函数声明、函数表达式与箭头函数函数是前端代码组织的基本单元。三种常见写法各有适用场景// 函数声明会被提升可以先调用后声明 function add(a, b) { return a b; } // 函数表达式不会提升 const subtract function (a, b) { return a - b; }; // 箭头函数ES6 推荐写法 const multiply (a, b) a * b; console.log(add(1, 2)); // 3 console.log(subtract(5, 2)); // 3 console.log(multiply(2, 3)); // 6箭头函数还有一个特点是不绑定自己的this。在事件回调、定时器、异步函数中箭头函数会继承外层作用域的this这是它比普通函数更符合直觉的地方。5.2 默认参数与剩余参数function greet(name 陌生人) { console.log(你好${name}); } greet(); // 你好陌生人 greet(张三); // 你好张三 function sum(...numbers) { return numbers.reduce((total, n) total n, 0); } console.log(sum(1, 2, 3, 4)); // 105.3 闭包与作用域链闭包是 JavaScript 面试高频题也是后端转前端必须理解的机制。闭包的本质是一个函数记住了它创建时所处的作用域中的变量即使在外部调用依然能访问。function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3这个例子中createCounter 内部返回的函数形成了一个闭包count变量不会被垃圾回收每次调用都会自增。这个模式对应到实际开发就是模块化封装、防抖节流、缓存函数等场景的核心原理。6. 数组与对象操作6.1 数组常用方法前端开发中超过八成对数据的处理都是在对数组做操作。重点掌握这些方法可以大幅减少 for 循环const users [ { name: Alice, age: 28 }, { name: Bob, age: 17 }, { name: Charlie, age: 24 } ]; // map批量转换 const names users.map(user user.name); console.log(names); // [Alice, Bob, Charlie] // filter筛选 const adults users.filter(user user.age 18); console.log(adults); // 包含 Alice 和 Charlie // find查找第一个匹配项 const findBob users.find(user user.name Bob); console.log(findBob); // { name: Bob, age: 17 } // reduce汇总 const totalAge users.reduce((sum, user) sum user.age, 0); console.log(totalAge); // 69 // some / every条件判断 const hasAdult users.some(user user.age 18); // true const allAdult users.every(user user.age 18); // false6.2 对象解构与展开运算符处理接口返回数据时对象解构非常常用const response { code: 0, data: { list: [1, 2, 3], total: 3 }, message: success }; // 解构赋值只拿需要的字段 const { code, data, message } response; const { list, total } data; // 展开运算符复制对象 const newResponse { ...response, timestamp: Date.now() }; console.log(newResponse);6.3 JSON 字符串与对象互转浏览器和服务器通信本质上是传字符串。JSON 序列化和反序列化是必须掌握的技能。常见场景是 localStorage 只能存储字符串要存储复杂对象必须先转成 JSONconst obj { name: localStorage 数据, list: [1, 2, 3] }; // 对象转 JSON 字符串 const jsonString JSON.stringify(obj); localStorage.setItem(myData, jsonString); // JSON 字符串转对象 const stored localStorage.getItem(myData); const parsed JSON.parse(stored); console.log(parsed.name); // localStorage 数据7. DOM 操作与事件处理7.1 获取页面元素DOM 是 JavaScript 操作页面的入口。要修改页面内容先要拿到对应的 DOM 节点!DOCTYPE html html head meta charsetUTF-8 titleDOM 操作演示/title /head body div idapp h2 classtitleJavaScript 基础教程/h2 button idbtn点击我/button ul idlist/ul /div script src./main.js/script /body /html对应的main.js// 通过 id 获取 const app document.getElementById(app); const btn document.getElementById(btn); // 通过选择器获取 const title document.querySelector(.title); const firstBtn document.querySelector(button); // 获取一组元素 const allLi document.querySelectorAll(ul li); // 修改文本和样式 title.textContent 修改后的标题; title.style.color #e74c3c; title.style.fontSize 24px;注意querySelector返回第一个匹配项querySelectorAll返回 NodeList。NodeList 虽然长得像数组但不是真正的数组需要用扩展运算符或者Array.from转换后才能使用 map、filter 等数组方法。7.2 动态渲染列表实际开发中最常见的场景就是拿到数据后渲染成页面列表。用数组的map方法批量生成列表项const courses [ { id: 1, name: HTML }, { id: 2, name: CSS }, { id: 3, name: JavaScript } ]; const list document.getElementById(list); const html courses.map(item { return li${item.name}/li; }).join(); list.innerHTML html;这里要提醒一个容易被忽略的性能问题如果列表数据量很大每次都通过innerHTML拼接字符串会引起整块 DOM 重新解析性能较低。更好的做法是使用DocumentFragment分段插入或者后续学习 Vue/React 时直接交给框架处理。7.3 事件监听与事件委托给按钮绑定事件是交互页面的基础btn.addEventListener(click, function () { alert(按钮被点击了); });事件委托是前端面试高频考点。原理是利用事件冒泡机制给父级绑定一次监听就能处理所有子元素的事件。这样即使动态新增的元素也能响应ul idtodoList li>const todoList document.getElementById(todoList); todoList.addEventListener(click, function (event) { const target event.target; if (target.tagName LI) { console.log(点击的项目 ID:, target.dataset.id); console.log(内容:, target.textContent); } });使用事件委托的好处有两个一是减少事件监听器数量避免给每个 li 都绑事件二是对动态新增的元素无需重新绑定。后面学 Vue 时框架内部的虚拟 DOM diff 也遵循类似的批量处理思想。8. ES6 常用语法与模块化8.1 模板字符串模板字符串简化了字符串拼接const user 张三; const age 25; // 旧写法 const msg1 你好 user 你的年龄是 age 岁。; // ES6 模板字符串 const msg2 你好${user}你的年龄是${age}岁。; console.log(msg2);模板字符串支持多行文本支持在${}中写任意表达式比如${age 18 ? 成年 : 未成年}。8.2 let、const 与块级作用域let 和 const 的出现解决了 var 的历史问题。来看一个计算器按钮的经典案例// 使用 var 的错误案例 var btns1 document.querySelectorAll(.btn); for (var i 0; i btns1.length; i) { btns1[i].addEventListener(click, function () { console.log(i); // 无论点击哪个按钮都输出最后一个索引 }); } // 使用 let 的正确案例 const btns2 document.querySelectorAll(.btn); for (let i 0; i btns2.length; i) { btns2[i].addEventListener(click, function () { console.log(i); // 正常输出对应索引 }); }var 的问题是函数作用域加变量提升循环结束后 i 已经变成最终值let 每次循环都会创建一次新的绑定闭包捕获的值就是当次循环的 i。8.3 模块化基础模块化让 JavaScript 代码可以拆分成多个文件按需引用。这是从“会写脚本”到“能维护项目”的关键一步// utils.js export function formatDate(date) { return ${date.getFullYear()}-${date.getMonth() 1}-${date.getDate()}; } export const VERSION 1.0.0;// main.js import { formatDate, VERSION } from ./utils.js; const today new Date(); console.log(formatDate(today)); console.log(当前版本: ${VERSION});这里要说明浏览器的原生 ES Module 需要通过typemodule方式引入如果是本地直接用file://协议打开 HTML可能会遇到跨域限制。最简单的解决方式是用 VS Code 的 Live Server 插件启动一个本地服务。9. 异步编程callback、Promise 与 async/await9.1 为什么需要异步JavaScript 是单线程语言同一时间只能做一件事。如果遇到耗时的网络请求、文件读取、定时器不能阻塞后续代码必须采用异步方式。最常见的一个失败示例是 setTimeout 输出顺序console.log(开始); setTimeout(function () { console.log(定时器执行); }, 0); console.log(结束); // 输出顺序开始 - 结束 - 定时器执行即使定时器延时为 0也会等当前同步任务执行完再执行。理解这个顺序对理解事件循环机制非常重要也是前端面试题 JavaScript 运行时报错的排查基础。9.2 回调地狱问题早期异步代码使用回调函数逻辑复杂时会出现多层嵌套这就是回调地狱// 反面教材 getUser(1, function (user) { getPosts(user.id, function (posts) { getComments(posts[0].id, function (comments) { console.log(comments); }); }); });9.3 Promise 改进Promise 用链式调用代替多层嵌套function getUser(id) { return new Promise((resolve, reject) { setTimeout(() { resolve({ id: id, name: 张三 }); }, 300); }); } getUser(1) .then(user { console.log(获取用户:, user); return user.id; }) .then(userId { console.log(用户ID:, userId); }) .catch(error { console.error(请求失败:, error); });Promise 有三种状态pending进行中、fulfilled成功、rejected失败。状态一旦改变就不能再变。9.4 async/await 最终方案async/await 是在 Promise 之上的语法糖让异步代码看起来和同步代码一样async function loadData() { try { const user await getUser(1); const posts await getPosts(user.id); const comments await getComments(posts[0].id); console.log(comments); } catch (error) { console.error(加载失败:, error); } } loadData();使用 async/await 时必须用try...catch捕获错误。忘记加异常处理是很多入门同学在接口调用失败时找不到原因的主要情况。9.5 fetch 与 API 请求实际项目中使用 fetch 请求后端接口是最常见的场景。下面是一个通用调用模板async function fetchData() { try { const response await fetch(https://api.example.com/users, { method: GET, headers: { Content-Type: application/json } }); if (!response.ok) { throw new Error(HTTP 错误状态码${response.status}); } const data await response.json(); console.log(data); return data; } catch (error) { console.error(请求失败:, error); } }排错建议如果 fetch 状态码是 200 但解析失败优先检查接口返回内容是不是合法 JSON如果网络正常但接口报错优先在浏览器 Network 面板看请求头和响应体。10. 实战项目构建一个待办事项应用10.1 功能需求与页面结构教程最后的核心实战案例是构建一个待办事项应用。通过这个项目可以把前面的变量、数组方法、DOM 操作、事件监听、本地存储全部串起来。页面结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title待办事项应用/title style body { font-family: system-ui, sans-serif; max-width: 500px; margin: 40px auto; padding: 0 20px; } .todo-input { display: flex; gap: 8px; margin-bottom: 20px; } .todo-input input { flex: 1; padding: 8px; } .todo-list { list-style: none; padding: 0; } .todo-list li { display: flex; justify-content: space-between; align-items: center; padding: 10px; border: 1px solid #eee; border-radius: 6px; margin-bottom: 8px; } .completed { text-decoration: line-through; color: #999; } /style /head body h1待办事项/h1 div classtodo-input input typetext idtodoInput placeholder输入新的待办事项 button idaddBtn添加/button /div ul idtodoList classtodo-list/ul /body /html10.2 核心逻辑数组、DOM 与事件委托const todoList document.getElementById(todoList); const todoInput document.getElementById(todoInput); const addBtn document.getElementById(addBtn); // 使用数组存储待办事项 let todos []; // 从 localStorage 恢复数据 function loadTodos() { const stored localStorage.getItem(todos); if (stored) { todos JSON.parse(stored); } } // 保存到 localStorage function saveTodos() { localStorage.setItem(todos, JSON.stringify(todos)); } // 渲染列表 function render() { const html todos.map((todo, index) li class${todo.completed ? completed : }>Uncaught TypeError: Cannot read properties of null (reading addEventListener)这种报错说明document.getElementById没有找到对应元素通常是因为 script 标签放到了元素前面执行时 DOM 还没加载完成。排查方法是把 script 放到 body 底部或者用DOMContentLoaded事件包裹。11.2 console 进阶用法调试不要只会console.log这几个方法更实用const user { name: 张三, age: 25 }; const list [HTML, CSS, JavaScript]; // 直接查看对象结构 console.table(user); console.table(list); // 分组输出 console.group(用户信息); console.log(姓名:, user.name); console.log(年龄:, user.age); console.groupEnd(); // 断言检查 console.assert(1 1 2, 数学错误);加了debugger可以让代码在指定位置暂停执行配合 Sources 面板逐步查看变量值变化这是理解复杂流程的重要方式。11.3 资源占用与性能观察前端项目没有模型部署的显存概念但依然要关注页面性能。在浏览器 Performance 面板可以查看脚本执行时间、内存占用和页面渲染耗时。数组批量渲染数据时如果页面出现明显卡顿优先检查是否在循环里做了频繁的 DOM 操作是否有多余的事件监听器是否有循环引用导致内存泄漏性能提升的通用手段包括批量生成 DOM 时使用DocumentFragment事件使用委托避免不必要的innerHTML全量重渲染图片懒加载。// 使用 DocumentFragment 批量添加 DOM const fragment document.createDocumentFragment(); for (let i 0; i 1000; i) { const li document.createElement(li); li.textContent 项目 ${i 1}; fragment.appendChild(li); } todoList.appendChild(fragment);从这套基础教程开始就应该养成“先跑通、再看性能”的习惯。功能正确是第一优先级性能优化建立在可运行代码之上。12. 常见问题与排查方法问题现象可能原因排查方式解决方案页面报错Cannot read properties of nullscript 标签位置在 DOM 元素之前检查 script 标签位置和执行时机将 script 放到 body 末尾或使用DOMContentLoaded包裹点击按钮无反应事件绑定失败或元素选择错误Console 打印按钮是否获取成功用document.querySelector确认选择器准确页面数据刷新后丢失没有使用 localStorage 持久化检查 localStorage 是否有数据使用localStorage.setItem保存状态异步接口数据拿不到没有正确处理 Promise 或 async/awaitNetwork 面板检查接口返回检查响应格式添加 try/catch 和 awaitforEach 中使用 return 无法跳出循环forEach 不支持 break/return只会跳过本次打印索引和值观察执行次数改用for...of或some()方法提前终止浏览器跨域请求失败本地文件协议或接口未开启 CORS查看 Network 请求状态使用 Live Server 启动本地服务JSON.parse报错localStorage 中没有合法 JSON 字符串手动查看存储内容先判断是否为空再解析或用 try/catch 包裹页面加载慢DOM 渲染过大或脚本执行复杂Performance 面板分析批量 DOM 插入减少重排这里单独展开两个出现频率最高的问题。forEach中return不能终止循环这个坑在开发中被反复踩。因为forEach的回调函数是独立执行作用域return只能跳出当前这次回调不能跳出整个循环。需要提前终止时用for...of。跨域问题是入门同学接触接口调用时最容易困惑的。如果在 HTML 里直接引用本地 JSON 文件用浏览器双击打开会因为file://协议的限制无法加载。解决办法是用 VS Code 的 Live Server 插件或者使用 Node.js 写一个静态服务器。13. 最佳实践与后续学习路线13.1 基础阶段的最佳实践先小步验证。每学一个新语法不要一次性写一大段而是先在 Console 里跑一个最小例子确认结果符合预期再放到项目里。比如学reduce先用[1, 2, 3].reduce((a, b) a b, 0)跑通再处理复杂数据。保留最小可运行示例。给自己的每个知识点建一个单独的小文件。代码越短定位问题越快。很多人学完 JS 基础后无法独立完成项目原因就是把所有代码堆在一个页面上出了问题完全找不到头绪。使用严格模式。在 JavaScript 文件开头标注use strict;可以让代码的常见错误直接在运行时暴露而不是静默失败。例如给只读属性赋值、使用未声明变量严格模式下都会报错。13.2 从基础到框架的衔接这份教程学完后建议按以下阶段继续前进先用原生 JavaScript 写一些更完整的项目比如轮播图、Tab 切换、手风琴菜单、留言板。再学习 TypeScript理解给动态语言加类型约束的好处。然后进入 Vue 或 React 框架重点关注组件化、声明式渲染和响应式数据。最后补齐工程化知识Vite、Webpack、ESLint、Prettier、Git。JS 基础阶段最常见的学习误区是“觉得懂了就不写代码”。前端开发是实践学科任何语法只有敲过、报错、修好才能真正内化成自己的经验。教程附带的源码和文档是很好的参考但有价值的不是把源码看懂而是先自己写一遍再对照源码找差距。14. 总结这套 JavaScript 基础教程最值得跟练的地方是把变量、数据类型、函数、数组、DOM、ES6 和异步编程这几个核心模块压缩进了一条清晰的学习路径并且附带了源码和文档每一步都有代码可以验证。如果你现在刚开始学前端建议先照着环境准备搭好工具按章节跑完代码再把实战项目独立重写一遍。最容易踩的坑集中在类型转换、this 指向、异步顺序和 DOM 选择失败这几块对应上面的排查表逐一解决即可。后续可以继续向 TypeScript、Vue/React、Node.js 服务端方向扩展。把这套 JS 基础吃透再接触框架和工程化会明显更顺畅。建议先收藏这篇文章需要写代码时照着 7、9、10 三个章节的示例边写边改效果比单独看视频好得多。