
1. 第十三天的转折点从网页骨架到网页灵魂如果你是按部就班学到第13天那么此刻你应该已经对HTML标签和CSS样式有了基本的掌控力。能写静态页面能调样式甚至能搞定几个常见布局。但站在我个人的学习经历回头看真正让我觉得“开始做前端了”的瞬间不是写出第一个漂亮的页面而是在编辑器里敲下第一行console.log(hello world)的那天——也就是今天JS基础。JavaScript这门语言在前端的位置很特殊。HTML负责内容结构CSS负责视觉表现而JS负责行为交互。你可以把前两者理解为“静态的骨架和皮囊”JS则是让页面活过来的神经和肌肉。一个按钮点了没反应、一个表单提交后不刷新页面就更新数据、一个列表能按条件动态过滤这些体验全都依赖JS。如果你到第13天还没正式开始JS今天就是那个“从切图仔变成开发者”的临界点。这篇笔记我会把自己在第13天前后踩过的坑、理清的思路、反复看过的概念全部梳理出来包含核心语法、执行机制、实操案例和排错方法。内容中有大量代码示例建议打开编辑器跟着敲一遍别只看不练。前端这块脑子会了手不会的状态太常见了。2. 基础语法与对象思维先搞懂JS在“说什么”2.1 变量声明与数据类型let、const才是主角很多新手教程还在用var但实际开发里我几乎只用let和const。这三者的区别可以很直白地记var是“古老的坑”let是“可变的盒子”const是“不可变的引用”。// const 声明必须初始化且不能重新赋值 const PI 3.14; // PI 3.15; // 报错Assignment to constant variable // let 可以重新赋值 let count 0; count 1; // 没问题 // 块级作用域示例 { let a 10; var b 20; } console.log(b); // 20var 没有块级作用域 // console.log(a); // 报错a is not definedconst和let是块级作用域var是函数级作用域。这意味着在{}里用let声明的变量出了这个花括号就不存在了这在写循环、条件判断时能避免很多变量污染问题。对象和数组用const声明其实很常见因为const限制的是“绑定关系”对象内部的属性还是可以改的。const user { name: 张三 }; user.name 李四; // 合法可以修改属性 // user {}; // 报错不能重新绑定JS的类型系统是动态的一个变量可以随时从字符串变成数字。基本类型有string、number、boolean、null、undefined、symbol、bigint复杂类型主要是object数组和函数其实都是对象的一种。2.2 对象的概念属性、方法、引用传递热词里有人搜“js对象的概念”这确实是新手容易绕晕的地方。我当初的理解方式是对象就是“一组键值对的集合”键是字符串值可以是任意类型包括函数。当值是函数时我们叫它“方法”。// 对象字面量创建 const student { name: 小红, age: 18, score: { math: 90, english: 85 }, sayHello: function () { console.log(大家好我是 this.name); }, }; // 访问属性 console.log(student.name); // 小红 console.log(student[age]); // 18中括号写法 student.score.math 95; // 修改嵌套属性 // 调用方法 student.sayHello();初学者最容易踩的坑是“对象是引用传递”。简单说把一个对象赋值给另一个变量时并没有复制一份而是两个变量指向同一块内存区域。const a { count: 1 }; const b a; // b 和 a 指向同一个对象 b.count 100; console.log(a.count); // 100a 也被改了 // 浅拷贝方式 const c { ...a }; c.count 200; console.log(a.count); // 仍然是 100这个特性在写项目时特别重要。如果你不想让原对象被意外修改就得用展开运算符...或者JSON.parse(JSON.stringify(obj))做拷贝。但要记住展开运算符是浅拷贝嵌套对象内部的改动依然会影响原对象。2.3 函数声明式、表达式、箭头函数JS里函数是“一等公民”可以像普通值一样赋值给变量、作为参数传递、作为返回值返回。这个特性是后面理解回调函数和异步的基础。// 1. 函数声明 function add(a, b) { return a b; } // 2. 函数表达式 const subtract function (a, b) { return a - b; }; // 3. 箭头函数 const multiply (a, b) a * b;箭头函数没有自己的this它会继承外层作用域的this这正好解决了普通函数中this指向混乱的痛点。在事件回调、定时器、数组方法里我几乎一律用箭头函数。// 传统写法中 this 容易丢失 const obj { data: [1, 2, 3], print: function () { // 这里用 function 会导致 this 指向 window this.data.forEach(function (item) { // console.log(this); // this 不再是 obj }); // 用箭头函数就正常 this.data.forEach((item) console.log(this.data)); }, }; obj.print();2.4 字符串判断包括、开头、结尾以及大小写那些坑热词里有个“js判断字符串是否包含”这个需求极高频。核心方法有几种const str Hello, web前端开发; // 1. includes() —— 最常用区分大小写 console.log(str.includes(前端)); // true // 2. indexOf() —— 返回首次出现位置找不到返回 -1 console.log(str.indexOf(前端)); // 9 console.log(str.indexOf(Java)); // -1 // 3. startsWith / endsWith console.log(str.startsWith(Hello)); // true console.log(str.endsWith(开发)); // true // 4. 正则表达式 test console.log(/前端/.test(str)); // true // 大小写不敏感的场景 const lowerStr str.toLowerCase(); console.log(lowerStr.includes(web)); // true需要特别提醒一点includes方法是区分大小写的如果想要不区分需要先统一转成小写或大写再判断。搜索功能里这个细节很常见处理不好用户会投诉“我搜大写搜不到”。2.5 数组方法map、filter、reduce替代for循环热词里出现“js map”说明大家确实都在补这块。过去我用for循环遍历数组代码长且容易出错学会map之后整个思路都变了。map的作用是对数组的每一项执行一个函数返回一个新数组不改变原数组。const numbers [1, 2, 3, 4, 5]; const doubled numbers.map((n) n * 2); console.log(doubled); // [2, 4, 6, 8, 10]与map配套的高频方法filter过滤出符合条件的项返回新数组reduce把数组归约为一个值求和、计数、分组都很方便forEach只遍历不返回新数组const nums [10, 22, 35, 41, 50]; // 过滤大于30的 const big nums.filter((n) n 30); // [35, 41, 50] // 求和 const sum nums.reduce((acc, cur) acc cur, 0); // 158 // 同时拿到索引 nums.map((n, index) console.log(index, n));map拿到的新数组长度和原数组一致如果你需要改变长度就用filter或者配合flatMap。要学会用这些方法关键在于改变“我要遍历然后如何如何”的思维变成“我要对数据流做哪些变换”。2.6 可选链与空值合并告别“Cannot read property”热词里有个“js ?. 作用”指的就是可选链操作符。这个语法在2020年左右全面普及如今已经是日常必备。它的作用很简单如果?.前面的值是null或undefined整个表达式会短路返回undefined而不是抛错。const user { profile: { name: 王五, address: null } }; // 传统写法 const town user.profile user.profile.address ? user.profile.address.city : 未知; // 可选链写法 const town2 user.profile.address?.city ?? 未知; console.log(town2); // 未知最典型的场景是从接口返回的数据中读取深层字段。后端返回的数据结构稍微一变或者某个字段为空传统写法就会整个页面崩溃。加上?.和空值合并运算符??之后代码稳健太多。const city response?.data?.user?.address?.city ?? 默认城市;这里??和||有区别||会把0、、false也当成“假值”而走默认值而??只处理null和undefined。比如用户年龄为0用||就会错误地变成默认值。3. 执行机制与异步编程前端进阶的必经之路3.1 执行上下文与变量提升“js 执行上下文与变量提升”这个关键词几乎每个前端面试都会考。理解它不需要背定义关键是记住JS引擎在执行代码前会做一个“预编译”动作把var声明的变量和function声明的函数提升到作用域顶部。console.log(a); // undefined而不是报错 var a 10; console.log(foo()); // hello函数声明完全提升 function foo() { return hello; }变量提升只提升声明不提升赋值所以a打印出来是undefined。函数声明则整个函数体都被提升所以调用可以发生在声明之前。let和const也有提升但存在“暂时性死区”即在声明之前访问会报错“Cannot access a before initialization”。这个设计其实是为了强制开发者先声明后使用避免var那种“写得很随意”的隐患。console.log(b); // ReferenceError: Cannot access b before initialization let b 20;执行上下文还包括this的绑定、作用域链的建立。作用域链可以理解为当前函数内部没有的变量会去外层找一直找到全局。嵌套函数形成的这个链就是闭包实现的基础。3.2 闭包一个让初学者“哇塞”的概念闭包没有想象中的神秘。当一个函数能够记住并访问它定义时的作用域即使这个函数在定义的作用域之外被调用就形成了闭包。function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); counter(); // 1 counter(); // 2 const counter2 createCounter(); counter2(); // 1每个counter独立上面的count变量没有被垃圾回收因为返回的函数仍然引用着它。这在做数据私有化、防抖节流、惰性加载时特别有用。不过闭包也会造成内存占用循环里创建闭包要小心最好用let声明循环变量。3.3 async/await与Promise把回调地狱踩在脚下热词里出现“js的async异步”不奇怪异步是JS学习里的一道坎。JS是单线程语言但通过事件循环机制可以把耗时任务网络请求、定时器、文件读取挂起等结果出来后再执行回调。最原始的异步写法是回调函数但当多个异步操作有依赖关系时回调嵌套会迅速变成“回调地狱”// 回调地狱示例 getUser(1, function (user) { getOrders(user.id, function (orders) { getOrderDetail(orders[0].id, function (detail) { console.log(detail); }); }); });Promise的出现就是为了解决这个问题getUser(1) .then((user) getOrders(user.id)) .then((orders) getOrderDetail(orders[0].id)) .then((detail) console.log(detail)) .catch((error) console.error(error));而async/await是Promise的语法糖让异步代码写起来像同步代码可读性大幅提升async function loadOrderDetail() { try { const user await getUser(1); const orders await getOrders(user.id); const detail await getOrderDetail(orders[0].id); console.log(detail); } catch (error) { console.error(加载失败, error); } }await只允许在async函数里使用。任何async函数都会返回一个Promise所以调用它时别忘了await或.then。实际开发中有个常见错误在forEach里用await期望并行处理结果顺序执行还不报错。如果需要并行请求用Promise.allconst [userInfo, orderList] await Promise.all([ getUser(1), getOrders(1), ]);3.4 fetch与JSON转换与后端对话的标准姿势热词里“js fetch”和“js json转换成数组”都很高频。fetch是现代浏览器内置的HTTP请求方法用来从服务器获取资源或提交数据。// GET 请求 async function fetchUsers() { try { const response await fetch(https://api.example.com/users); if (!response.ok) { throw new Error(请求失败状态码 response.status); } const data await response.json(); // 解析JSON console.log(data); } catch (error) { console.error(网络错误或解析错误, error); } } // POST 请求 async function createUser() { const newUser { name: 赵六, age: 20 }; const response await fetch(https://api.example.com/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(newUser), }); const result await response.json(); console.log(result); }这里涉及两个关键点response.json()返回的也是一个Promise要await发送数据时要用JSON.stringify把对象转成字符串拿到数据后要JSON.parse或直接调用.json()把字符串转回对象/数组。很多新手直接用fetch(url)后立刻res.json()没检查response.ok结果接口返回404或500时报错信息不直观。记住只要网络请求成功fetch都会返回response哪怕状态码是500。必须手动判断response.ok或response.status。JSON和数组的转换还有一个高频坑后端返回的是一段JSON字符串而不是对象时前端会拿到一个“看得到点不进去”的数据。此时先用JSON.parse(responseText)转一下注意要放在try...catch里因为字符串格式一不对就会抛异常。4. 综合实战从零写一个学生管理系统的核心逻辑4.1 需求拆解热词里有人问“js里怎么设计一个学生管理系统实现学生信息的增删改查”这很适合作为第13天的综合练习。它不是要你写一个完整后端而是用JS在浏览器端实现数据管理逻辑。核心功能添加学生表单输入姓名、年龄、成绩删除学生点击按钮移除对应记录修改学生点击编辑回填表单保存更新查询学生按姓名或学号过滤列表这个项目几乎覆盖了JS基础中所有核心知识点数组遍历、对象操作、DOM操作、事件处理、数据渲染。4.2 用数组作为数据源先把数据模型定义好// 用数组模拟数据库 let students [ { id: 1, name: 张三, age: 18, score: 88 }, { id: 2, name: 李四, age: 19, score: 92 }, ]; // 生成自增id let nextId students.length 1; // 增添加学生 function addStudent(name, age, score) { const student { id: nextId, name, age, score }; students.push(student); renderList(); } // 删按id删除 function deleteStudent(id) { students students.filter((s) s.id ! id); renderList(); } // 改按id更新 function updateStudent(id, newData) { const index students.findIndex((s) s.id id); if (index ! -1) { students[index] { ...students[index], ...newData }; } renderList(); } // 查按关键字过滤 function searchStudents(keyword) { const kw keyword.trim().toLowerCase(); if (!kw) return students; return students.filter( (s) s.name.toLowerCase().includes(kw) || String(s.id).includes(kw) ); }用filter、findIndex、展开运算符等方式比直接改下标更安全。修改数据后别忘了重新渲染列表这是前端数据驱动视图的核心循环。4.3 渲染列表与事件委托渲染部分需要操作DOM。第13天你可能刚开始接触DOM这里给出一套比较标准的结构function renderList(list students) { const tbody document.querySelector(#studentTable tbody); tbody.innerHTML ; // 清空旧内容 list.forEach((s) { const row document.createElement(tr); row.innerHTML td${s.id}/td td${s.name}/td td${s.age}/td td${s.score}/td td button classedit-btn>tbody.addEventListener(click, (event) { const target event.target; if (target.classList.contains(delete-btn)) { const id Number(target.dataset.id); deleteStudent(id); } else if (target.classList.contains(edit-btn)) { const id Number(target.dataset.id); const student students.find((s) s.id id); fillForm(student); // 回填表单 } });用dataset存自定义数据是DOM操作中的常用技巧注意>document.querySelector(#fileInput).addEventListener(change, function (e) { const file e.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload function (event) { const csvText event.target.result; // 按行拆分再按逗号拆分注意引号包裹的字段 const rows csvText.split(\n).map((line) line.split(,)); // 去掉表头行 rows.shift(); rows.forEach((row) { addStudent(row[1], Number(row[2]), Number(row[3])); }); }; reader.readAsText(file); });真实场景中CSV里的逗号、引号、换行都可能造成解析错误所以复杂格式建议直接用SheetJS库。不过基础思路是一样的把文件读成文本或二进制再转换成数据结构。5. 常见问题与排查技巧实录5.1 用控制台快速定位JS错误写JS最痛苦的就是“页面没反应也不知道哪里错了”。第一个动作永远是打开浏览器开发者工具F12切到Console面板。那里会直接告诉你错误类型、错误信息、出错的行号。常见的几种错误信号Uncaught TypeError: Cannot read properties of undefined访问了不存在对象里的属性检查前端拿到的数据是不是undefinedUncaught ReferenceError: xxx is not defined变量没声明或拼写错误Failed to fetch跨域问题或网络请求失败NaN在数值运算中混入了字符串或者Number()转换失败配合console.log在关键位置打印中间结果是排查逻辑错误最朴素也最有效的手段。要是涉及循环或函数调用的过程设置断点后单步调试更好用。5.2 JS引入失败云路径改本地路径后访问不了热词里有一条很现实“有一个项目将云路径改为本地路径后js访问不了如何解决”。这个问题我在本地开发时也遇到过多次。造成的可能性其实有几种首先看浏览器的Network面板找到那个JS文件的请求看状态码是404还是500。如果是404大概率是路径写错了。前端页面里按相对路径读取资源是相对于“当前页面的URL”来解析的而不是相对于HTML文件所在目录。比如页面在/pages/index.html你写script src./js/app.js实际会请求/pages/js/app.js这可能不是你要的位置。其次是路径中带中文或空格浏览器会自动编码服务器端处理不好就404。建议所有文件和文件夹都用英文小写加连字符。还有一种情况是本地服务器的静态资源目录配置问题尤其是你用Node或Python的静态服务时要确认JS文件放在了被服务的根目录下。最后检查引用顺序。如果JS写在DOM结构之前而代码一上来就去操作DOM元素会报“Cannot read property of null”因为元素还没有加载。解决办法是把script标签放到/body之前或者给代码套上DOMContentLoaded事件document.addEventListener(DOMContentLoaded, function () { // 在这里操作DOM });5.3 iframe刷新父页面跨层级通信技巧热词里有一条“iframe关闭jquery并刷新父页面js”。iframe在日常开发中越来越少见但接手老项目时还是会遇到。场景一般是父页面用iframe嵌入了子页面子页面操作完成后需要刷新父页面或关闭iframe窗口。原生JS写法如下// 在子页面iframe内中执行 // 刷新父页面 window.parent.location.reload(); // 或者只刷新父页面的某个区域比如父页面里有函数 refreshList window.parent.refreshList(); // 获取父页面的jQuery对象 window.parent.$(#list).append(li新数据/li);如果是跨域页面直接访问window.parent会报跨域错误这时只能通过postMessage通信。但同域情况下上面的写法是最快路径。关闭iframe窗口则可以由父页面处理// 子页面中通知父页面 window.parent.closeIframe(); // 父页面中定义函数 function closeIframe() { const iframe document.getElementById(myIframe); iframe.parentNode.removeChild(iframe); }5.4 省市区三级联动数据结构与渲染策略“省市区编码和名称js数据”和“js三级联动”也是很常搜的内容。三级联动的本质是“根据上级选择动态渲染下级选项”。这本身不难难在数据组织。常见的两种数据格式第一种是平铺数组每一项带有parentCode字段const regions [ { code: 110000, name: 北京市, parentCode: null }, { code: 110100, name: 市辖区, parentCode: 110000 }, { code: 110101, name: 东城区, parentCode: 110100 }, ];第二种是嵌套树形结构每一项包含children数组。实际接口返回大多为平铺数组需要前端自己构建树或通过filter按父级筛选。联动思路const provinceSelect document.querySelector(#province); const citySelect document.querySelector(#city); const districtSelect document.querySelector(#district); // 初始化省份 function initProvinces() { const provinces regions.filter((r) r.parentCode null); provinces.forEach((p) { const option document.createElement(option); option.value p.code; option.textContent p.name; provinceSelect.appendChild(option); }); } // 省份变化时动态更新城市 provinceSelect.addEventListener(change, function () { const provinceCode this.value; const cities regions.filter((r) r.parentCode provinceCode); renderOptions(citySelect, cities); districtSelect.innerHTML ; // 重置区县 }); function renderOptions(selectEl, list) { selectEl.innerHTML option value请选择/option; list.forEach((item) { const option document.createElement(option); option.value item.code; option.textContent item.name; selectEl.appendChild(option); }); }如果数据量很大每次都filter全量数据性能也不差但更好的做法是提前用Map把父子关系索引起来查询复杂度变成O(1)。5.5 关于“js逆向”、反爬与技术底线热搜词里出现了“js逆向”“js反爬实战”“ctfhub js前端验证”等。作为学习笔记我想简单聊两句。前端JS逆向和反爬确实是一个真实存在的技术方向它涉及分析网页请求、加密参数、混淆代码等。学一学HTTP协议、加密算法基础、浏览器调试技巧对理解前端安全有很大帮助这部分本身是中性技术知识。但我要明确提醒一点把技术用在非法抓取他人数据、绕过付费验证、破解会员权限这是违反法律法规和服务条款的行为。踩过界的问题一旦发生轻则账号封禁重则有法律风险。我写这篇笔记的原则就是只聊正当技术学习不做任何越界的示范。希望你在学习时也多把这些知识用在防护自己的站点、理解系统安全机制上。实际上理解JS逆向确实能反过来提升前端功底。你会更注意敏感信息不要写在前端代码里更理解为什么很多接口要做签名校验也更能体会“前端代码永远不可信”这句话的含义。但这种理解应该建立在安全防御的视角而不是攻击视角。5.6 工具链初体验编辑器与调试热词里提到“vscode 做网页前端开发 如何查看web界面代码构成的界面”。这属于开发环境问题。VSCode是目前前端开发最主流的编辑器之一。查看网页代码构成本质上就是打开浏览器开发者工具中的Elements面板它会把渲染后的DOM树、CSS样式、盒模型完整展示出来。在VSCode里的日常流程装插件Live Server本地起一个静态服务器改代码自动刷新页面、ESLintJS代码规范检查、Prettier格式化代码按F12或右键“检查”打开浏览器开发者工具Console看报错Sources打断点Network看请求Application看存储调试JS最推荐的方式是在Sources面板的代码行号上点击设置断点然后刷新页面代码会在断点处暂停。此时可以查看所有局部变量的值点击“Step Over / Step Into”逐步执行比console.log只打日志高效得多。6. 几个必须养成的编码习惯第13天这个阶段除了语法知识习惯比知识点本身更重要。我说几个自己摸索出来的实用习惯。第一个是“小步快跑”式写代码。不要等写了一百行再一起测试而是每写一个函数就在控制台或页面上验证一下。JS是动态语言很多类型错误是运行时才暴露的长时间不运行错误堆积后会非常难定位。第二个是保持命名清晰。变量名和函数名不要用a、b、data这类无意义命名更不要用拼音缩写。一个getStudentById的函数名比一大堆注释都有用。代码多半是写给别人和维护未来的自己看的命名习惯直接影响可维护性。第三个是“先静态后动态”的调试顺序。页面出了交互问题先检查HTML结构对不对、CSS是不是把元素挡住了再检查JS有没有报错、数据格式是否符合预期。很多时候你以为的JS问题其实是选择器选错元素。第四个是善用console.table和console.dir。普通对象用console.log看起来不够直观console.table可以用表格展示数组对象console.dir可以查看对象的详细属性结构。这些小工具能显著提升排查效率。7. 第十三天的学习成果自测清单你可以拿下面这些题目检验一下自己的掌握程度能解释let、const、var的区别吗能不能随口说出两种以上能写一个函数接收一个字符串返回包含某个关键字的所有数组项吗能在不查资料的情况下用map把一组对象里的某个字段提取成新数组吗能解释“对象是引用类型”的含义并写一个浅拷贝/深拷贝的示例吗能用async/await写一个fetch请求并对异常做处理吗能说清楚“变量提升”是什么以及let的暂时性死区是怎么回事吗能独立完成上面的学生管理系统增删改查吗如果这些问题里有一半以上需要犹豫说明还得回头再看一遍对应小节。不用急JS基础就是要反复过几轮才能内化。13天只是开始往后的项目实战才是真正的练功场。今天把这些基础概念打牢后面接触框架Vue、React时会轻松很多因为框架本质上也在用这些东西。有个体会可以提前告诉你学JS最忌“眼高手低”。看别人的代码觉得都能看懂自己动手就卡壳这是非常正常的阶段。解决方式只有一个——写大量地写。今天这一篇笔记里的每个代码片段都建议手动敲一遍哪怕敲完觉得“这不是一样的吗”敲的过程和看的过程在大脑里留下的痕迹完全不同。等哪天你不用看文档也能随手写出一个联动表单、一个数据列表渲染你就正式迈过JS基础这道门槛了。