多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JavaScript学习笔记:字符串包含、数组排序与异步编程

JavaScript学习笔记:字符串包含、数组排序与异步编程 说实话JavaScript 这门语言我已经写了挺多年但真正把学习笔记整理成一条完整知识线还是最近的事。网上一搜“js 判断字符串是否包含”“js 数组排序”这类零散问题一大把可初学者照着抄完代码下个场景照样懵。我这份笔记的核心思路是不把语法当考点背而是围绕一个真实小项目把数据类型、运算符、函数、数组操作、异步流程和工程场景串起来。无论你刚接触前端还是写过一点 HTML 和 CSS 想补上 JS 这块拼图这份笔记都能帮你少走很多弯路。1. 整体思路把 JS 当成“给网页打招呼的语言”很多人学 JavaScript 的第一个动作是打开控制台打印一句console.log这没问题。但我更推荐在动手之前先建一个知识地图知道 JS 在网页里到底扮演什么角色。1.1 先搞懂 HTML、CSS、JS 的关系我一直喜欢用这个类比HTML 是毛坯房CSS 是装修JS 是水电和智能家居。HTML 负责结构比如页面上有一个按钮、一个输入框、一个列表。CSS 负责外观比如按钮是蓝色还是圆角、列表是横排还是竖排。JS 负责行为比如点击按钮后弹窗、输入关键词后筛选列表、提交表单后更新页面。如果你只写 HTML 和 CSS页面是静态的用户怎么点都不会有反应。一旦引入 JS页面就“活”了。在学习过程中我强烈建议每学一个 JS 特性都立刻想一个使用场景判断字符串是否包含关键词能用来做搜索框数组排序能用来给列表按时间排列操作对象取值能用来读取后端返回的数据。1.2 用一个例子贯穿所有知识点这份笔记的“主线任务”是一个待办事项管理器。它需要做这几件事输入框输入内容点击按钮后把内容加入列表用到数据类型、数组操作、DOM 操作搜索框输入关键词只显示匹配的条目用到字符串判断点击“排序”按钮按创建时间或者内容长度排序用到数组排序删除重复条目用到数组合并去重处理加载数据时的延迟用到同步异步一个小项目几乎覆盖了 80% 的基础语法。比起背知识点这种“我要做功能→需要什么语法→查笔记→写代码→调试”的路径记忆留存率高得多。1.3 学习路径怎么规划我复盘自己走过的弯路总结了一个比较顺的阶段顺序你可以在笔记目录里按这个顺序浏览语言基础数据类型、运算符、条件、循环函数进阶回调、作用域、执行上下文、闭包数据操作字符串方法、数组方法、对象取值异步编程回调函数、Promise、async/await工程实践引入方式、DOM、浏览器 API、常见工具库调试排查阅读报错、使用 DevTools、定位问题下文就按照这个顺序把每一块的关键点和踩坑经验都讲清楚。2. 语言地基数据类型、运算符和流程控制这部分是 JS 最容易“一看就会一用就废”的地方。因为表面语法和直觉差别不大但细节坑特别多。2.1 数据类型typeof 的五个坑JS 的原始类型有七种实际使用中你至少要熟练区分这几种string、number、boolean、null、undefined、object、symbol我当年最容易被typeof骗到下面这些结果都记在小本本上表达式typeof 结果实际原因typeof 42number数字正常typeof hellostring字符串正常typeof trueboolean布尔正常typeof undefinedundefinedundefined 是独立类型typeof nullobject历史遗留 bugnull 不是对象typeof []object数组本质是对象typeof function(){}function函数特殊处理判断一个变量是不是数组不要用typeof要用Array.isArray()。判断 null 要用 null。这些属于“语法背不出来但调试时会被反复教做人”的经验。2.2 运算符 和 的差距以及 ?? 的妙用新手最容易犯的错误写条件判断时用了。JS 里的会做隐式类型转换规则极其反直觉。比如0 0 // true 0 // true false // true null undefined // true我建议业务代码里一律用和!这样既避免隐式转换的坑也让代码语义更清晰。想判断“值相等且类型相同”就用这是最稳的写法。还有一个运营频率很高的运算符是空值合并??。它和||看起来像但差别很大const a 0; const b a || 10; // 10因为 0 被当成 falsy const c a ?? 10; // 0因为 ?? 只处理 null 和 undefined我在做配置项默认值的时候如果这个值可能是 0 或者空字符串一定用??而不是||否则用户填了 0 也会被无脑替换掉。2.3 条件语句与循环语句学会写“人能读懂”的代码JS 的条件语句常见有三种if...else、switch、三元表达式? :。if...else适合复杂逻辑switch适合对同一个值做多个分支判断三元表达式适合简单的二选一赋值循环语句也一样至少掌握四套写法// 传统 for 循环 for (let i 0; i arr.length; i) { console.log(arr[i]); } // for...of 循环推荐直接拿值 for (const item of arr) { console.log(item); } // for...in 循环注意一般用来遍历对象键名别用来遍历数组 for (const key in obj) { console.log(key, obj[key]); } // forEach 循环不能中途 break arr.forEach((item, index) { console.log(index, item); });我自己的习惯是数组用for...of或forEach对象用Object.keys()或者for...in。for...in遍历数组会把原型链上的可枚举属性也带上很容易出意想不到的 bug初学者尽量避开。3. 函数是 JS 的灵魂定义、回调与执行上下文如果只能挑一个章节让人反复读我一定会选函数。JS 的函数不仅是一段可复用逻辑更是一等公民可以赋值给变量、作为参数传递、作为返回值返回。3.1 函数定义的几种方式和使用区别常见四种写法// 1. 函数声明 function add(a, b) { return a b; } // 2. 函数表达式 const add function (a, b) { return a b; }; // 3. 箭头函数 const add (a, b) a b; // 4. 构造函数方式不推荐日常使用 const add new Function(a, b, return a b);这里的核心区别有两个。第一函数声明会“提升”也就是在定义之前调用也不会报错。函数表达式和箭头函数不行它们在执行到那一行之前变量是undefined。第二箭头函数没有自己的this它继承定义时所在作用域的this。这个特性在写事件回调时特别省心但也导致它不能用来做构造函数。3.2 回调函数不立即执行而是“回头再调”回调函数这个概念我第一次看定义觉得玄乎其实一句话就能讲清楚把一个函数作为参数传给另一个函数对方在合适的时机调用它。举一个日常例子function processUserData(userId, onSuccess, onError) { // 模拟异步获取数据 setTimeout(() { const data { id: userId, name: 张三 }; onSuccess(data); }, 1000); } processUserData( 1, (data) console.log(拿到用户, data.name), (err) console.error(出错了, err) );这里onSuccess和onError就是回调函数。按钮点击、定时器、网络请求全是基于回调的。理解了回调后面再学 Promise 就不费力。3.3 执行上下文与作用域链为什么循环里的“var i”是坑执行上下文可以理解为 JS 引擎在运行代码时的“环境快照”里面包含变量、函数声明、this指向等。全局有全局执行上下文每次调用函数都会创建一个新的函数执行上下文。作用域链则决定了变量从哪里找当前函数内找不到就往定义它的外层找一直找到全局作用域。这个机制带来了“闭包”也带来经典的var循环陷阱for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出 3 3 3而不是 0 1 2原因就是var声明的i属于函数作用域循环结束后i已经是 3每个 setTimeout 回调记住的其实是同一个变量。解决方法是把var换成let让每次循环都产生一个块级作用域或者用立即执行函数包一层。3.4 闭包既好用又容易被忽视闭包就是“函数 它定义时所处的作用域”。最简单的闭包function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2外部函数执行完后内部的count并没有被垃圾回收因为返回的函数还持有对它的引用。日常写防抖、节流、封装私有变量都在用闭包。但也要注意闭包用多了可能会让变量长期占用内存如果不再使用最好把引用置空。4. 字符串与数组处理数据的日常操作写业务代码最花时间的不是语法而是处理用户输入和后端返回的数据。字符串判断、数组排序、合并去重、对象取值基本是每天必用。4.1 判断字符串是否包含includes、indexOf 还是正则初学者最常见的问题“js 判断字符串是否包含”用哪个方法我直接给结论只判断是否包含includes需要知道位置或统计次数indexOf忽略大小写先toLowerCase()再includes复杂模式匹配正则test()const str Hello JavaScript World; console.log(str.includes(Script)); // true console.log(str.indexOf(Java)); // 6没有则返回 -1 console.log(str.toLowerCase().includes(hello)); // true console.log(/j.*s/i.test(str)); // true正则忽略大小写一个小细节includes第二个参数可以指定开始搜索的位置。比如判断aaa.includes(aa, 1)结果也是true因为从索引 1 开始还能匹配到aa。这个参数在处理“排除开头匹配”时很有用。4.2 数组排序sort() 的默认行为容易出乎意料很多人直接用arr.sort()然后发现 10 排在 2 前面。因为sort()默认是把元素转成字符串后按 Unicode 编码排序所以数字2会排在10前面。正确写法必须传比较函数const arr [10, 2, 22, 3]; // 升序 arr.sort((a, b) a - b); // 降序 arr.sort((a, b) b - a);按对象属性排序也一样const tasks [ { title: 写笔记, createdAt: 20230105 }, { title: 复习, createdAt: 20230102 }, ]; tasks.sort((a, b) b.createdAt - a.createdAt); // 按时间倒序注意sort()会修改原数组。想保留原数组先[...arr].sort(...)再做后续操作这是高频踩坑点。4.3 数组合并去重Set 是你的好朋友数组去重方法很多最简洁的写法是用Setconst arr1 [1, 2, 3]; const arr2 [2, 3, 4]; // 合并 const merged [...arr1, ...arr2]; // 合并并去重 const unique [...new Set(merged)]; // [1, 2, 3, 4]对象数组去重不能用这个办法因为每个对象引用不同。需要根据某个字段去重const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 }, { id: 1, name: 张三 }, ]; const map new Map(); users.forEach((u) map.set(u.id, u)); const deduplicated [...map.values()];4.4 获取对象的所有值别手动循环了经常有后端返回一个对象比如{ 任务A: 3, 任务B: 5 }你只想把值收集成一个数组。用三件套const obj { taskA: 3, taskB: 5, taskC: 8 }; const keys Object.keys(obj); // [taskA, taskB, taskC] const values Object.values(obj); // [3, 5, 8] const entries Object.entries(obj); // [[taskA, 3], ...]需要“获取字典的所有值”时Object.values()一行搞定。需要同时用键和值做处理就解构entriesObject.entries(obj).forEach(([key, value]) { console.log(key, value * 2); });5. 同步与异步理解 JS 的运行机制“js 同步和异步”这个话题很多同学问得最多其实理解起来并不难关键看能不能分清“谁先执行”和“谁后执行”。5.1 为什么需要异步JS 是单线程语言同一时间只能做一件事。如果网络请求要等 2 秒期间不让用户操作页面就会卡死。所以 JS 把耗时任务交给浏览器去处理等任务完成后再把回调放进一个队列里这就是异步的基础思路。异步编写方式经典的三个阶段回调函数 → Promise → async/await。5.2 事件循环记住一个顺序就够了事件循环可以简化成一句话同步代码先执行然后不断从任务队列里取出任务执行。微任务的优先级高于宏任务。console.log(1); setTimeout(() console.log(2), 0); Promise.resolve().then(() console.log(3)); console.log(4); // 输出结果1 4 3 2为什么Promise.then输出在setTimeout前面因为 Promise 的回调属于微任务而setTimeout属于宏任务。浏览器执行完一段同步代码后会先把所有微任务执行完再去取宏任务执行。5.3 Promise 和 async/await告别回调地狱简单理解 Promise 就是一个容器里面装的是一段异步操作。它有三个状态pending进行中、fulfilled成功、rejected失败。状态一旦改变就不会再变。function fetchTask() { return new Promise((resolve, reject) { setTimeout(() { resolve(下载完成); }, 1000); }); } fetchTask() .then((data) console.log(data)) .catch((err) console.error(err));async/await只是让 Promise 的链式写法更可读async function load() { try { const data await fetchTask(); console.log(data); } catch (err) { console.error(err); } }注意await只能在async函数里用。如果循环里需要串行请求可以用for...of配合await如果多个互不依赖的请求用Promise.all并发执行速度会快很多。5.4 实战案例用异步处理待办事项的“假请求”回到我们的待办事项项目假设要从服务端拿任务列表我用一个setTimeout模拟延迟async function loadTasks() { const list document.getElementById(taskList); list.innerHTML 加载中...; try { const tasks await new Promise((resolve) setTimeout(() resolve([买牛奶, 写周刊, 读书]), 800) ); renderTasks(tasks); } catch (err) { list.innerHTML 加载失败请重试; } }调接口的时候切忌“拿到接口就写在render里”一定要有加载态和错误态。用户不会喜欢一个点了没反应的按钮。6. 工程场景把 JS 放进真实项目学基础语法只是第一步真正让人印象深刻的是把 JS 和各种浏览器 API、外部库结合起来解决实际问题。6.1 三种引入方式怎么选行内事件处理button onclickalert(hi)适合演示不推荐用于复杂逻辑内联脚本script...代码.../script适合单页面小功能外部脚本script srcapp.js/script推荐正式项目使用外部脚本里有个常见报错“script 标签放在 body 底部或者使用defer属性”因为脚本执行时如果 DOM 还没解析完document.getElementById会拿到null。script srcapp.js defer/scriptdefer会等到 HTML 解析完成后再下载执行脚本是当前最推荐的引入方式。6.2 DOM 操作打开 URL、关闭弹窗、刷新父页面浏览器页面上高频操作就几类打开新页面window.open(url)或者设置location.href url在当前页面跳转location.href url新标签页打开window.open(url, _blank)关闭当前弹窗window.close()还有一个实际项目经常遇到的需求父页面打开了一个 iframe 弹窗操作完成后要关闭 iframe 并刷新父页面。代码很简单但很多人不知道写在哪// 在 iframe 内部执行 window.parent.location.reload(); // 刷新父页面 window.parent.closeModal(); // 调用父页面的弹窗关闭方法如果想更“正规”一点用window.parent.postMessage()通知父页面执行逻辑避免直接耦合父页面的全局函数。6.3 操作 Excel推荐 xlsx 库“操作 excel 的 js 工具库 - xlsx 的使用方法”这个问题几乎每周都有人搜。最常用的是SheetJS安装方式一句npm install xlsx读取文件的核心三步import * as XLSX from xlsx; const fileInput document.getElementById(upload); fileInput.addEventListener(change, (e) { const file e.target.files[0]; const reader new FileReader(); reader.onload (event) { const data new Uint8Array(event.target.result); const workbook XLSX.read(data, { type: array }); const sheet workbook.Sheets[workbook.SheetNames[0]]; const json XLSX.utils.sheet_to_json(sheet); console.log(json); }; reader.readAsArrayBuffer(file); });生成并导出一个 Excel 也一样简洁const worksheet XLSX.utils.json_to_sheet([ { 标签: 学习, 数量: 3 }, { 标签: 工作, 数量: 5 }, ]); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 分类统计); XLSX.writeFile(workbook, 统计.xlsx);用这个库做数据导出比服务端生成 Excel 再下载简单很多。一个小提醒文件较大时最好在 Web Worker 里处理避免菜单卡顿。6.4 现代框架入门从 Angular 到 Vue 的版本问题不少人在笔记里记了“angular9 与 node js 的版本”这个问题。Angular 对 Node.js 版本有要求一般官方文档都会标注推荐版本Node 版本太老或者太新都可能触发依赖安装报错。遇到这类问题第一反应去查官方文档的 compatibility 表格而不是自己瞎猜。Vue 的核心思想则很适合 JS 学习者数据驱动视图。你只需要改数据页面自动更新这背后本质上还是 JS 的响应式原理和 DOM 操作只是封装好了。学习路线建议先用原生 JS 写小项目再去看 Vue 文档会发现自己已经能读懂大半原理。7. 常见报错与调试排查DevTools 是最好的老师很多人学 JS 最崩溃的不是语法而是“页面白屏了但不知道错在哪”。这时候别慌打开浏览器开发者工具按 F12切到 Console 面板所有报错都列在那里。7.1 高频报错速查表报错信息常见原因快速排查xxx is not defined变量名拼错或未声明检查声明位置和作用域Cannot read properties of null元素没找到拿不到 DOM检查 script 标签位置、id 名jquery-3.6.1.min.js 404资源路径不对右键查看请求地址确认文件是否真的存在Unexpected token语法错误比如中文标点看报错行号逐字符排查is not a function变量不是函数却被调用打印该变量看类型Cannot read properties of undefined对象没定义或层级不对先用console.log打印完整对象文件找不到的问题比语法错误更隐蔽。比如本地项目里引了script srcjs/jquery-3.6.1.min.js实际文件却在js/lib/目录下就会 404。我建议打开 Network 面板看请求路径再对着项目文件目录一步步比对基本都能解决。7.2 调试流程先找现象再定位范围遇到 bug不要直接在源码里乱改。我的标准调试流程是复现问题记录触发步骤。打开 Console 面板看第一行报错。点报错右侧的链接跳到 Sources 面板对应行。在可疑位置打console.log或打断点观察变量值。修正后验证顺手跑一遍相关功能。断点调试比console.log更强的地方在于它能暂停在指定行左侧面板可以看到所有局部变量的实时状态还能单步执行。调试陌生页面代码时这种方式尤其有效。7.3 学会“读”别人的 JS 代码不管你是接手前辈的项目还是在网上看示例代码都要养成先总后分的习惯。先看整体结构哪些是变量声明哪些是函数定义哪些是入口调用再挑核心函数逐行阅读最后在关键节点打断点观察运行过程。遇到陌生项目先在 Sources 面板看加载了哪些 JS 文件顺着入口文件往下追。这个过程不需要特殊工具但是对理解别人的语法风格和代码组织方式帮助很大。我自己的学习笔记里有相当一部分知识就是在这种“读代码 打断点 改代码”的循环中学到的。8. 最后想分享的一点学习习惯如果要让我给这份“js 学习笔记”一个真正的核心那我会说不要为了学而学而是为了做一个东西而学。每次遇到新语法就想想能不能把它用到你的待办事项项目、个人主页或者小工具里。遇到报错不要急着复制粘贴关键词找答案先尝试自己读一遍报错信息。我在整理笔记的过程中反复体会到JS 的知识点之间是互相连通的字符串方法、数组方法、函数回调、异步流程、DOM 操作最终都汇入同一个目标——让页面更好地响应用户的操作。希望这份笔记也能成为你自己知识体系的第一块基石之后无论去学 Vue、Angular 还是 Node都会发现底层还是这些基础的东西在支撑。
返回列表