
JavaScript 这个东西我学了好几年也写了三年多的业务代码回头看最大的感受是网上到处是xx天精通js的教程但真正能让一个普通开发者在实际项目中少走弯路的内容反而是那些零散的热搜词——什么 js 判断字符串是否包含、数组排序的几种方法、执行上下文、同步异步、回调函数……这些词单个看都很小但组合起来其实就是一份非常真实、非常接地气的前端查漏补缺清单。今天这篇js学习笔记我不想写成一本从头到尾的教材而是想用一个从业者的视角把那些高频搜索、高频踩坑、高频面试的点串起来理清思路讲透原理附上可以直接复用的代码片段顺便聊聊我自己踩过的一些坑。适合谁适合刚学完基础语法、却写不利索实战代码的人也适合工作了一两年、想系统梳理js知识体系的前端开发者。1. 为什么这份笔记值得看先理清 js 的学习优先级1.1 热搜词背后的真实需求信号我平时有个习惯会定期去看一下前端相关的搜索热词。你可以把它当成一份群众用脚投票的需求调查。搜索热度最高的一批词其实透露了几个有意思的信号。第一类是基础语法相关的词比如html css js基础语法js数据类型js运算符js循环语句js函数。这说明大量初学者仍然卡在最基础的语法关口上。很多人以为学 js 就是记住几个 console.log、写个 for 循环但真正一脱离教程去写小项目就会发现连变量声明用 var 还是 let 都想半天更别说去理解对象、数组和函数之间的关系。基础语法不是背会就行的它需要的是理解它背后的数据存储逻辑和运行规则。第二类是进阶机制相关的词比如js执行上下文js同步和异步js回调函数实例js宏。这类词的搜索量一直很高说明很多人学完语法之后在js 到底是怎么运行的这个问题上遇到了瓶颈。你可以把语法理解为单词和句型但执行上下文、调用栈、事件循环这些才是 JS 这门语言真正的运行机制。不理解这些你写出来的代码经常会出现明明按顺序写的结果执行顺序却不是我想的那样的困惑。第三类是日常业务开发的高频需求词比如js数组排序的几种方法js数组合并去重js判断字符串是否包含js 获取字典的所有值js三级联动iframe关闭并刷新父页面。这类词对应的是真正的日常开发场景。搜索引擎上为什么有这么多人搜就是因为这些代码天天写但总会忘今天写了明天忘明天写了后天还得查。它们的核心价值不是难而是高频。第四类是工具链和框架相关问题比如js引入angular9与node js的版本outlook ollama js sdk操作excel的js工具库 - xlsx的使用方法。这说明现代前端开发已经不能只写原生 js 了你至少得知道怎么引入脚本、怎么用第三方库、怎么处理版本兼容。这一块是很多人从会写 js迈向会做项目的最后一公里。把这些信号汇总起来你会发现一件事js 的学习曲线其实不是一条直线而是基础语法 → 运行机制 → 高频场景 → 工程化这四层。很多人学不下去不是因为笨而是因为把顺序搞反了——基础没吃透就去背框架源码运行机制没搞明白就去搜各种奇技淫巧。1.2 我给初学者设计的 js 学习节奏结合我自己的经验和前面讲的热词分布我建议的学习路线是这样的阶段一把核心语法学扎实。变量、数据类型、运算符、条件分支、循环、函数这是地基中的地基。不要追求快追求每一项都能用自己的话说清楚。阶段二搞懂运行机制。执行上下文、作用域、闭包、this、同步异步。这一阶段不能光看书必须配合断点调试亲眼看代码一步步跑。阶段三练高频开发场景。数组操作、字符串处理、对象遍历、DOM 操作、事件处理、表单交互。这些是你在业务开发中最常用的二十个能力点建议整理成本地代码片段库。阶段四接触工程化和框架。script 标签引入、模块化、npm 包、打包工具然后选 Vue 或 React 中的一门深入下去。这份笔记主要覆盖的是阶段一到阶段三的核心内容同时补一些阶段四的实用经验。我在写的过程中会有意把热搜词转换成知识点方便你按图索骥。2. js 核心语法体系从数据类型到语句的查漏补缺2.1 深入理解 js 数据类型别再被对象和数组坑了我们先从最基础的 js 数据类型讲起。很多人背过基本数据类型有 Number、String、Boolean、Undefined、Null、Symbol、BigInt引用类型有 Object但真到用的时候还是会栽跟头。我见过一个最经典的坑就是判断两个对象是否相等直接用或者比较。这是不行的因为引用类型比较的是内存地址不是内容。const a { name: 张三 }; const b { name: 张三 }; console.log(a b); // false因为 a 和 b 指向不同的内存空间你可以用生活化类比来理解基本类型就像你在纸上写了两张纸条上面都写着数字 3纸条之间的内容可以相等。而引用类型就像两个人各拿了一把钥匙钥匙指向的是房间里堆的东西哪怕两个房间里的东西一模一样只要不是同一个房间钥匙就不相等。在开发中判断引用类型是否值相等正确做法是手动逐字段比较或者用 JSON.stringify 转换后比较但要注意 JSON.stringify 对属性顺序敏感也可以用 Lodash 的isEqual。我自己在写表单数据校验的时候最常用的方案是 JSON.stringify因为它够快也够直观只要保证对象里的 key 顺序一致就行。再说typeof和instanceof。typeof对一些类型会返回让人误解的结果比如typeof null返回objecttypeof []返回object。这是 JS 诞生时就存在的历史遗留问题没必要死记里面的怪异之处但你要知道以后凡是遇到判断一个变量是不是某个类型的需求都不要想当然地写 typeof而是要结合多种判断方式。比如判断数组用Array.isArray()判断一个对象是不是普通对象可以用Object.prototype.toString.call(obj)。2.2 运算符与条件循环中那些反直觉的细节js 运算符这一块热搜词里出现了js运算符还有js ??。这里我想重点聊聊几个实际开发里很容易踩坑的运算符。比较运算符是重灾区。会做隐式类型转换不会。比如0 在 JS 里返回 true0 返回 false。我在早期学习中一直不明白为什么大家总被建议永远用 。其实不是因为 完全不能用而是因为 的隐式转换规则太复杂你根本记不住与其把时间花在记这些规则上不如统一用 把这个思考成本直接砍掉。ES2020 新增的两个运算符也建议尽快用起来。空值合并运算符??只有当左边的值是 null 或 undefined 时才返回右边的值。它和||最大的区别是||会在左边是 0、、false 这些假值时都触发右边而??只会在 null 和 undefined 时触发。举个例子const a 0; const b a || 默认值; // 默认值因为 0 是假值 const c a ?? 默认值; // 0因为 0 不是 null/undefined这个区别在处理接口返回的数值时非常有用。比如一个商品的折扣价是 0你希望显示 0 而不是默认价这时候用 ?? 才是正确的。可选链运算符?.更是日常写代码的救命稻草。以前我们取一个可能不存在的嵌套属性得写if (obj obj.data obj.data.list)现在直接写obj?.data?.list。如果中间某一层是 null 或 undefined表达式整体返回 undefined不会抛错。这个操作符配合空值合并是现在主流前端代码里出场率最高的两个语法点。再说循环语句。js 里的循环有 for、while、do...while、for...in、for...of 五种很多人写来写去只会用 for。其实他们之间有不同的适用场景for...of用于遍历数组、字符串、Set、Map 等可迭代对象for...in用于遍历对象会遍历到原型链上的可枚举属性这是个坑所以通常要配合hasOwnProperty使用。我建议的记法是数组用 for...of对象用 Object.keys() 或者 Object.entries() 配合 for...offor...in 这个语法尽量少用因为它带来意外问题的概率大于它带来的便利。3. 函数、作用域与执行上下文js 运行机制的核心3.1 从函数声明到函数表达式远没你想的那么简单js 函数是面试和日常工作的核心。热搜词里js函数javascript学习手册八js函数js回调函数实例都是高频搜索。但我的观察是很多人会写函数却不理解函数在 JS 里的本质。函数在 JS 里是一等公民意味着你可以把它当作值来传递。这句话听起来玄乎其实很简单你可以把函数名赋给另一个变量也可以把函数作为参数传给别的函数还可以把函数作为返回值从别的函数里返回。函数声明和函数表达式的区别必须记住。函数声明会提升hoisting你可以在声明之前调用它函数表达式不会提升你只能在赋值之后调用。这是很多初学者写代码时突然报错 cannot access before initialization的根源。// 函数声明可以提前调用 say(你好); function say(word) { console.log(word); } // 函数表达式必须先赋值再调用 sayHello(你好); // TypeError: sayHello is not a function const sayHello function(word) { console.log(word); };回调函数也是一个必懂概念。回调的本质就是把函数当参数传给别人别人在合适的时机调用它。我常用外卖类比你点了外卖留了手机号回调函数外卖员做好菜后打电话给你调用回调电话接通后你就可以去取餐了。在 JS 里最常见的回调场景就是事件监听、定时器、Ajax 请求。比如setTimeout(() { console.log(到点了) }, 1000)这里的箭头函数就是一个回调浏览器在 1 秒后会调用它。3.2 执行上下文、调用栈与 this 指向理解前必须搞懂的运行机制js执行上下文这个热搜词连续几年热度都很高足见它是公认的难点。我自己的理解经历了一个从背诵概念到真正明白的过程这里分享一个我认为最容易讲清楚的角度。执行上下文可以理解为JS 代码运行时的一个环境记录。每当你调用一个函数JS 引擎就创建一个新的执行上下文里面包含了这个函数内的变量、参数、this 等信息。这些上下文会被压入一个调用栈当前正在执行的函数在最栈顶执行完了就弹出继续执行下一个。这就是为什么 JS 是单线程的同一时间只能做一件事。this 的指向是执行上下文里最让人头疼的部分。其实只需要记住一条核心规则this 的指向在函数被调用时才确定它指向调用这个函数的那个对象。普通函数调用时this 指向全局对象浏览器里是 window作为对象的方法调用时this 指向这个对象使用 call/apply/bind 时this 指向你传入的第一个参数箭头函数不绑定 this它会继承外层作用域的 this。const obj { name: 前端食堂, getName() { console.log(this.name); } }; obj.getName(); // 输出 前端食堂。 const fn obj.getName; fn(); // 输出 undefined因为此时 this 指向 windowwindow.name 是 undefined很多人写代码的时候突然发现 this 不对十有八九就是丢掉了调用对象——把一个方法单独取出来调用。解决方式有两种一是在调用时用obj.getName()的方式二是用箭头函数或 bind 把 this 固定到你想要的对象上。3.3 同步、异步、回调、宏任务与微任务js同步和异步也是热搜词。这个概念初看都能理解同步就是按顺序执行异步就是先放一边等条件满足后再执行。但真正写代码的时候很多人对异步代码为什么比同步代码后执行完感到困惑。我们从底层说。JS 引擎本身是单线程的但浏览器或者 Node.js除了 JS 引擎之外还有一堆其他线程比如定时器线程、网络请求线程、事件监听线程。当 JS 主线程碰到异步操作比如 setTimeout它会把回调交给相应的其他线程去计时自己接着往下走。等定时时间到了回调并不会立刻执行而是被放进一个任务队列。主线程把当前同步代码全部执行完毕后会去任务队列里拿回调来执行。这里就引出了宏任务和微任务的概念。JS 的任务队列分两种宏任务macrotask和微任务microtask。宏任务包括 setTimeout、setInterval、I/O、UI 渲染等微任务包括 Promise.then、MutationObserver 等。执行顺序的规则是每执行一个宏任务就清空一次微任务队列。所以Promise.then里的回调通常比setTimeout里的回调先执行。我看过一道非常经典的执行顺序题只要理解了基本就通了console.log(1); setTimeout(() { console.log(2); }, 0); Promise.resolve().then(() { console.log(3); }); console.log(4); // 输出顺序1, 4, 3, 2原因刚才已经讲了先执行同步代码 1 和 4然后 JS 引擎去微任务队列清空 Promise 回调输出 3最后才去宏任务队列执行 setTimeout输出 2。这个问题我建议每位学 js 的人都亲手跑一遍并且把其中的原理用自己的话讲出来。讲清楚了异步这块就算真正入门一半了。我在实际项目里踩过一个典型的异步坑在 Vue 里通过接口获取列表数据然后立即对列表进行筛选操作结果发现列表是空的。原因就是接口是异步的数据还没返回筛选代码已经执行完了。解决方式要么在 Promise.then 里操作要么用 async/await 等待接口完成后再操作。先把数据拿回来再处理数据这个顺序上的思维转变是最重要的一课。4. 日常开发中最常被搜索的高频 js 实用技能4.1 字符串、数组与对象操作开发中最常用的二十个细节说实话业务开发中写来写去用的最多的就是字符串、数组、对象之间的组合操作。热搜词里的js判断字符串是否包含js数组排序的几种方法js数组合并去重js 获取字典的所有值这些词搜索量居高不下说明这些点确实是很多人的日常痛点。先讲字符串。判断一个字符串是否包含某个子串ES6 之后直接用includes即可它返回布尔值。它比传统的indexOf() -1语义更清晰。const str Hello World; console.log(str.includes(World)); // true console.log(str.indexOf(World) -1); // true老写法如果需要忽略大小写最简单的方式是先把两个字符串都转成小写或大写然后比较或使用 includes。const str JavaScript; console.log(str.toLowerCase().includes(script)); // true验证 URL 是否有效这个其实包含两层意思一是格式上是否合法二是请求上是否能访问。格式判断可以用 URL 构造函数function isValidUrl(url) { try { new URL(url); return true; } catch { return false; } }至于能不能访问那就得发请求了前端里叫判断跨域、后端才能做真正的连通性检查。很多人在面试里被问到 URL 校验第一反应是写正则其实用 URL 构造函数更简单可靠。再讲数组。数组排序是经典问题Array.prototype.sort()默认将元素转成字符串后按 UTF-16 编码排序所以数字升序必须传比较函数const arr [3, 1, 10, 2]; arr.sort((a, b) a - b); // [1, 2, 3, 10] arr.sort((a, b) b - a); // [10, 3, 2, 1]这个 a 减 b 升序 的公式记住就行。对象数组按属性排序也大同小异。数组去重ES6 的 Set 是最简洁的方式const arr [1, 2, 2, 3, 3, 4]; const unique [...new Set(arr)]; // [1, 2, 3, 4]数组合并直接展开运算符或者 concat 都行。const arr1 [1, 2]; const arr2 [3, 4]; const merged [...arr1, ...arr2]; // [1, 2, 3, 4]如果想合并且去重就合并后再用 Set 包一层。这种组合操作在实际业务里非常常见比如把两个 Tab 的数据合并去重后再渲染。对象操作方面获取字典的所有值在 JS 里就是Object.values()同时可以搭配Object.keys()拿所有键Object.entries()拿所有键值对。这三个 API 在实际开发中的出场率极高const dict { name: 张三, age: 18, city: 北京 }; console.log(Object.keys(dict)); // [name, age, city] console.log(Object.values(dict)); // [张三, 18, 北京] console.log(Object.entries(dict)); // [[name,张三], [age,18], [city,北京]]遍历字典时我常用的模式是for (const [key, value] of Object.entries(dict)) { console.log(${key}: ${value}); }4.2 页面操作与组件思路DOM 操作、iframe、三级联动前端不只要处理数据还要操作页面。热搜词里js div里增加diviframe关闭并刷新父页面js三级联动js打开url这些都属于这一类。js div里增加div这其实就是 DOM 操作的入门动作。常用的方式有两种一种是innerHTML直接拼接字符串另一种是用createElement创建元素再 append。前者简单但容易造成 XSS 风险如果内容包含用户输入后者更安全、性能更好。我建议在项目里尽量用后者const parent document.getElementById(container); const child document.createElement(div); child.textContent 新的子节点; child.className item; parent.appendChild(child);说到iframe 关闭 刷新父页面这个需求在管理后台项目里出现频率非常高。比如在一个页面上弹个 iframe 对话框用户保存完数据后页面关闭父页面的列表需要同步刷新。核心技术点是 iframe 与父页面之间的通信// iframe 内部关闭并刷新父页面 window.parent.location.reload(); // 直接刷新父页面 // 如果父页面里嵌的是弹窗组件要触发父页面的关闭方法 window.parent.closeDialog(); // 需要父页面暴露 closeDialog 方法更进一步iframe 之间的通信可以用 postMessage这是更安全的方案因为它可以指定目标 origin避免收到不可信数据。跨窗口通信和高德地图、低代码平台里的页面嵌出本质都是同一套机制。js三级联动也很典型比如省市区选择。它的思路很简单先有第一级数据当第一级变化时根据第一级的 value 去请求或匹配第二级数据第二级变化后再匹配第三级数据。核心是联动即事件触发 数据筛选。我实现过一版一级数据变化后直接调用一个重置该级及以下所有级的函数避免出现选了三级但二级还是空的脏状态。js打开URL这个分两种情况同窗口跳转用window.location.href url新窗口打开用window.open(url)。但实际开发中弹窗经常会遇到浏览器拦截因为window.open必须在用户主动点击的事件回调里同步调用如果你在异步方法里调用很多浏览器会把它当成弹窗广告拦截掉。解决办法是同步先const win window.open(, _blank)等异步数据返回后再win.location url。对了还有js引入这个话题。基础阶段只要会用script srcxxx.js/script在 HTML 里引外部脚本就够了。但从工程化角度这个引入方式有全局污染和依赖顺序问题。现在主流做法是 ES Module 的方式script typemodule import { xx } from ./module.js; /script或者用打包工具Vite、Webpack统一管理。我建议的学习顺序是先搞懂 script 标签引入的原理再过渡到 import/export最后理解打包工具只是把一堆 import/export 文件打包成几个可运行的脚本而已。5. 新手期最容易踩的坑附排查思路5.1 报错与加载类问题排查表我在带新手的过程中发现大部分报错都逃不出那么几个套路。这里直接整理成一个排查表方便你照着查报错或现象常见原因解决方式xxx is not defined变量未声明、作用域不对、js 文件没加载成功检查加载顺序确认变量定义位置xxx is not a function调用的对象没有这个方法或函数表达式在赋值前被调用用 typeof 检查调整调用时机Cannot read property of undefined异步数据未返回就访问了对象属性用 ?. 可选链或者先做空值判断Cannot access before initializationlet/const 声明提升但未初始化形成了暂时性死区调整代码顺序声明后再使用中文乱码页面编码不一致html 里统一设置meta charsetUTF-8js 文件也保存为 UTF-8引用的第三方库找不到路径错误、没有安装依赖、CDN 失效检查路径、重新安装、换 CDN 或本地化引入开发中最典型的一类场景就是引用的 js 文件报 404。拿热搜词里提到的 kkfileview 预览 PDF 时找不到 jquery-3.6.1.min.js 这类问题举例它本质上是本地依赖文件路径和线上环境不一致导致的资源加载失败。排查的时候先看网络面板F12 → Network找到红色报错的请求确认实际请求的 URL 和磁盘里的文件路径是否一致。有时候是目录层级错了有时候是 CDN 地址没配好。这里我的建议是凡是需要稳定运行的项目前端静态资源一定要放自己的服务器或对象存储上不要过于依赖公共 CDN否则一换网络环境就很可能出幺蛾子。angular9 与 node js 的版本这类兼容性问题也常出现在老项目里。Angular 9 对应的 Node.js 版本并不是任意版本都兼容早期版本对 Node 版本有明确要求。遇到这类问题排查思路是先看项目的 package.json 里框架版本的 require 字段再查框架官方文档里对应 Node 版本要求表然后用 nvm 切换 Node 版本进行验证。这类问题的共性经验是版本兼容性问题永远是先查官方文档的版本对照表靠猜是浪费时间。5.2 逻辑类隐患这一层坑更隐蔽也更要命比报错更坑的是不报错但结果不对。这类问题大多出在数据比较、异步时序和引用类型上。比如NaN的比较。NaN NaN返回 false判断一个值是否为 NaN用Number.isNaN()。再比如浮点数精度。0.1 0.2不会等于 0.3这在涉及金额计算的业务里是致命伤。处理方式是用整数运算比如把元转成分或者引入 decimal.js 之类的库。这个坑我当年在写一个促销计算逻辑时就踩过后来统一改成了分单位计算问题才彻底消失。还有数组和对象的引用传递问题。用赋值数组后修改新数组会连带修改旧数组因为它们指向同一块内存地址。需要复制时数组要使用[...arr]或arr.slice()对象要用{ ...obj }。这点在 React、Vue 这种推崇不可变数据的框架生态里尤其重要。另外一个常见的逻辑坑是弹窗关闭后父页面数据不同步。处理方式就是前面提到的 iframe 刷新或者自定义事件但还有一个细节如果在 iframe 里用window.parent去调用父页面函数之前最好先确认一下window.parent ! window防止页面本身就是顶层窗口时出现意外行为。5.3 学会用浏览器工具的三个调试技巧排查问题最有效率的方式不是瞪眼看代码而是调试。这里分享三个我每天都会用的小技巧。第一console.log 也要讲技巧。不要在代码里打一堆无意义的 log至少把变量名一起打出来建议用对象形式console.log({ list, total })。这样控制台展示出来的不仅值清楚还带上了变量名排查效率高很多。第二学会用断点。在 Sources 面板里点击代码行号就可以打断点执行到那里会停住你可以在右侧查看所有变量的当前值也可以逐步执行。断点调试对理解执行上下文非常有帮助我建议所有学 js 的人都去体验几次执行一行看一次调用栈。第三用 Network 面板观察异步请求。接口返回的数据不对时先在 Network 里看响应体确认是后端给的数据不对还是前端处理逻辑不对这一步能帮你省下大量甩锅时间。6. 真正把 js 学好靠的不是记住而是写出来最后想把自己这几年学习中悟到的一点经验分享出来。很多初学者总在收藏各种js学习笔记js大全但收藏了不看看了不会用用了就忘形成了一个收藏—遗忘的循环。我的经验是学 js 最有效的动作是每天手写 20 行代码写什么都可以哪怕是用数组方法把今天要做的事处理一遍然后隔两天再去改一遍这 20 行代码。你会发现第一遍很难看第二遍就会顺手很多第三遍你已经能想到更优的写法。这个过程比我当初闷头看三个月教程有用得多。还有一个实用技巧整理一份自己的代码碎片库。不需要完整的小程序而是把那些高频的小片段保存起来比如数组去重、日期格式化、URL 参数解析、防抖节流。热搜词里为什么那么多人搜js判断字符串是否包含因为大家都不愿意每次重新发明轮子。如果你自己维护一个碎片库每次需要的时候直接复制改一改你的开发效率会提升得非常明显。这份碎片库其实比任何一本教材都更适合你。如果你正在学 js 的路上或者已经从入门到放弃了好几回别慌。这个领域确实有点碎但它的每一块碎片单独拿出来都不难难的是把它们串起来。希望这份笔记能帮你把那根线理得更清楚一点。