1. 先搞清楚JavaScript到底是个什么1.1 不是Java的JavaScript一段绕不开的过往先说个很多新手都问过的问题JavaScript和Java是什么关系答案可能让你失望——基本没关系。JavaScript是1995年Netscape公司的工程师Brendan Eich花十天时间设计出来的最初叫Mocha后来改名LiveScript为了蹭当时Java的热度才定名为JavaScript。这门语言的定位从来就是“让网页动起来”跟Java的“企业级应用开发”完全是两条路线。后来为了让这门语言有一个中立的标准化版本欧洲计算机协会给它起了个正式名字叫ECMAScript。你平时看到的ES5、ES6、ES2015这些说法其实都是在说JavaScript的不同版本。ES6也叫ES2015是一个重要的分水岭从它开始JavaScript加入了let/const声明、箭头函数、解构赋值、Promise、async/await这些现代特性整个开发体验提升了一大截。现在你写前端代码基本默认就是在写ES6及以上版本的语法所以网上搜资料时如果看到ES6别慌那就是JavaScript的现代版本代号。1.2 JavaScript能做什么、不能做什么今天JavaScript已经完全不是“只能在网页里弹个框”的小玩意儿了。在浏览器里它可以操作页面内容DOM、控制浏览器本身BOM、发网络请求、做动画在服务端Node.js让JavaScript跑在服务器上能写接口、操作数据库、做工程化工具还有Electron桌面应用、React Native/uni-app移动端、甚至用WebGPU做3D渲染。可以说前端领域的“三件套”——HTML结构、CSS样式、JavaScript行为JavaScript是唯一一门编程语言也是整个前端生态的基石。但它也有自己的边界。比如它运行在沙箱环境里不能直接读写本地文件Node.js除外、不能直接访问操作系统底层能力。它的单线程模型也决定了如果你在代码里写一个死循环整个页面就会卡死。理解了这些边界你就知道为什么网页应用和原生应用在使用体验上有差异也就能理解后面要讲的事件循环、异步编程到底在解决什么问题。1.3 浏览器里怎么运行JS三件套与运行机制浏览器拿到一个网页按顺序做三件事解析HTML构建DOM树、解析CSS构建样式规则、执行JavaScript。JavaScript脚本放在HTML里通常有两种方式一种是script标签直接写在HTML文件中适合小项目另一种是用src属性引入外部JS文件适合正式项目代码能和HTML分离维护起来也干净。你可能会在控制台看到“Please enable JavaScript to continue”这种提示这通常不是你的代码出了问题而是浏览器设置里把JavaScript停用了。Chrome地址栏输入javascript:这种伪协议比如热词里提到的javascript:void(o)也是同样的道理——如果地址栏直接执行JS被拦先检查浏览器安全策略而不是纠结代码本身。提示现在主流的开发方式已经不是“写个HTML再引入JS”而是用Webpack、Vite这类构建工具把多个JS模块打包。但不管工具怎么变底层运行的还是JavaScript本身所以打好语言基础永远是最值得做的事。2. 从零起步基础语法与数据类型2.1 一个最小的JS程序长什么样先来写第一个JS程序。在浏览器里按F12打开开发者工具切到Console控制台面板输入下面这一行再回车console.log(你好JavaScript);这一行代码干了一件事调用console.log方法把括号里的内容打印到控制台。console.log是你在JS生涯里最常用的调试工具没有之一。后面我们会反复用到它所以请你现在就记住凡是想要知道某个变量是什么、某个函数返回了什么第一反应就是console.log(它)。JS代码还有一个特点语句末尾的分号是可写可不写的很多自动化格式工具会自动帮你补全或删掉。这里我给的建议是新手阶段统一写上分号等你熟悉了构建工具和代码规范再决定要不要遵循“无分号”风格。保持风格统一比纠结分号本身重要得多。2.2 七种数据类型与typeof的坑JavaScript的数据类型分两大类原始类型Primitive和引用类型Reference。原始类型一共有7种string字符串、number数值、boolean布尔、null空、undefined未定义、symbol符号、bigint大整数。引用类型就是object对象数组、函数、日期等等在底层都是object。新手最容易踩的坑有两个。第一个坑是typeof null返回的是object这是这门语言从诞生起就存在的Bug但因为历史兼容性问题一直没修。第二个坑是浮点数精度问题0.1 0.2的结果不是0.3而是0.30000000000000004。这不是JavaScript独有的问题而是所有用IEEE 754标准存浮点数的语言都有的问题只是因为JS长期在前端运行被更多人感知到而已。遇到需要精确计算的场景比如金额我的建议是用整数存分而不是用浮点数存元或者借助decimal.js这类库。2.3 变量声明var、let、const怎么选声明变量有三种关键字var、let、const。我直接说结论新代码一律用const除非确认这个变量之后会被重新赋值才用let。不要用var。为什么这么说var存在两个很别扭的特性变量提升hoisting和没有块级作用域。变量提升的意思是你可以在声明之前使用一个var变量它的值是undefined没有块级作用域的意思是for循环里用var声明的变量循环结束后在外面还能访问到。这两个特性在代码复杂之后很容易埋雷。let和const是ES6引入的块级作用域变量const还额外保证“一旦赋值不能重新指向别的值”这对代码的可读性和可维护性帮助非常大。注意const保护的是“变量指向的引用不能变”如果你用const声明了一个对象你仍然可以修改这个对象的属性只是不能让这个变量重新指向另一个对象。3. 把逻辑串起来运算符、条件与循环3.1 运算符全家桶从算术到三元运算符是任何程序语言的基本零件。JavaScript的运算符主要分这几类算术运算符 - * / % --、比较运算符 ! !、逻辑运算符 || !、赋值运算符 - * /、三元运算符条件 ? 值1 : 值2。这里重点说两个不太直观但一天到晚用到的。第一个是%取余运算符它常被用来判断奇偶num % 2 0是否为偶数或者做循环队列的环形索引。第二个是三元运算符它本质上是if...else的简写适合场景简单、只有两个分支的情况。比如const status isLogin ? 已登录 : 未登录;但我不建议你在一行里嵌套多个三元运算符那种“三个问号连在一起”的代码当时写得很爽过两周自己看都要反应半天。嵌套超过一层老老实实写if...else或switch。3.2 与 一个永远值得你注意的区别和的区别是JavaScript新手面试题里出现频率最高的几个问题之一。简单说在比较时会做类型转换则严格要求值和类型都相等。举个例子console.log(1 1); // true因为1被转换成了数字1 console.log(1 1); // false一个是数字一个是字符串 console.log(null undefined); // true console.log(null undefined); // false这些类型转换规则非常复杂甚至有[] ![]为true这种奇葩结论。我的建议就是项目代码里一律用和!永远不要用。这样既能避免很多隐蔽的Bug也让你在代码评审的时候不用费口舌向别人解释到底有没有踩到类型转换的坑。3.3 条件语句与循环语句的结构选择条件语句主要是if...else if...else和switch。当你需要判断多个分支且每个分支都是等值比较时switch可读性更好但如果是范围判断、组合条件判断if更灵活。还有一个常被忽略的知识点switch里的case如果不写break会“穿透”继续执行下一个case这是很多隐蔽Bug的来源。要么每个case都写break要么故意利用穿透做多条件命中但一定要加注释说明。循环语句有for、while、do...while还有专门用来遍历对象的for...in和遍历数组的for...of。ES6之后的时代数组自带了很多遍历方法比如forEach、map、filter、reduce。我个人的习惯是需要生成一个新数组时用map需要过滤时用filter需要汇总计算时用reduce只有中途需要跳出循环或需要知道索引时才用传统的for。while一般用在“不知道要循环多少次只知道什么时候停”的场景。4. 组织数据的核心数组与对象4.1 数组从增删改查到高阶遍历数组在JavaScript里是一等公民学好数组基本就学会了前端数据处理的半壁江山。先看最基础的增删改查尾部增删push()/pop()对数组末尾操作。头部增删unshift()/shift()对数组开头操作。按索引改直接arr[1] 新值。按值查indexOf()、includes()、find()。任意位置增删splice(start, deleteCount, item1, item2)。splice是很多新手容易弄混的方法它直接修改原数组返回被删除的元素组成的数组。比如arr.splice(1, 1)就是从索引1开始删除1个元素arr.splice(1, 0, 新元素)就是在索引1的位置插入新元素。再来说遍历。ES6之后for...of可以直接遍历数组的值比传统的for (let i 0; i arr.length; i)更简洁。但如果同时需要索引我更推荐用forEach的回调参数。数组还有一个高频操作是合并字符串和拆分成数组join(分隔符)可以把数组拼成字符串split(分隔符)可以把字符串拆成数组。热词里的“javascript 合并字符串”也和这个有关后面我们专门讲字符串时再展开。4.2 对象属性和方法的基本功对象可以理解为“一组键值对的集合”。创建对象的方式有几种字面量、new Object()、构造函数、Object.create()实际开发里百分之九十九的场景都用字面量因为最直观const user { name: 张三, age: 25, sayHello() { console.log(你好我是${this.name}); } }; console.log(user.name); // 张三 console.log(user[age]); // 25 user.sayHello(); // 你好我是张三访问属性有两种方式点语法user.name和方括号语法user[name]。当属性名是不合法的变量名比如user[my-name]或者是动态计算的字符串时只能用方括号。这一招在处理接口返回的动态键名时特别有用。this是对象方法里最让人头疼的概念。简单记一个原则在普通函数中this指向调用该函数的对象。user.sayHello()时this就是user但如果把方法取出来单独调用const fn user.sayHello; fn()this就变成了undefined或全局对象。ES6的箭头函数不绑定this它会沿用定义时外部作用域的this很多回调场景下用箭头函数能少踩不少坑。4.3 剩余参数与展开运算符让代码更清爽热词里提到了“javascript 剩余参数”这是ES6里我非常喜欢的一个特性。剩余参数用...表示可以把函数接收到的多个参数收进一个数组里function sum(...nums) { return nums.reduce((total, n) total n, 0); } console.log(sum(1, 2, 3, 4)); // 10展开运算符的语法也是...用法却刚好相反把数组或对象“拆开”。const arr1 [1, 2, 3]; const arr2 [4, 5]; const merged [...arr1, ...arr2]; // [1, 2, 3, 4, 5] const obj1 { a: 1, b: 2 }; const obj2 { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }这和热词里的“javascript 合并字符串”也有点关系——[...str]可以把字符串拆成字符数组再配合join()和splice做字符级操作。展开运算符在做数组浅拷贝、对象合并、函数传参时都特别好用建议你把它当成标配来记。5. 字符串、正则与JSON文本处理的三个利器5.1 字符串方法大盘点拼接、截取、替换字符串是前端处理最多的数据类型之一用户输入、接口返回大部分都以字符串形式存在。先盘点最常用的方法拼接、concat()、模板字符串${a}${b}。截取slice(start, end)、substring(start, end)、substr(start, length)。替换replace(旧值, 新值)、replaceAll(旧值, 新值)。查找indexOf()、includes()、startsWith()、endsWith()。大小写转换toLowerCase()、toUpperCase()。去空格trim()。拆分成数组split(分隔符)。热词里的“javascript 合并字符串”如果你搜到的是concat方法它和最大的区别是的任意一侧只要有一个字符串另一个操作数就会被自动转成字符串concat则是严格参数必须是字符串如果传了非字符串会报错。现在模板字符串已经普及我的建议是用模板字符串做拼接它支持换行、支持嵌入变量和表达式可读性比用拼一长串强太多。replace有个非常重要的细节如果第一个参数传的是字符串它只会替换第一处匹配要想替换全部要么用replaceAll要么传正则并在正则后面加g标志。这种坑我在代码评审里见过不止一次。5.2 正则表达式匹配与捕获入门正则表达式可以理解成“字符串里的模糊匹配规则”。在JavaScript里创建正则有两种方式字面量/pattern/和构造函数new RegExp(pattern)。日常开发中字面量更常用因为它不用转义反斜杠。常用的几个APIregex.test(str)测试字符串是否匹配返回布尔值。str.match(regex)返回匹配结果数组。str.replace(regex, newSubstr)用正则替换。str.search(regex)返回首次匹配的索引。举个例子校验手机号以1开头、11位数字const phoneReg /^1\d{10}$/; console.log(phoneReg.test(13812345678)); // true console.log(phoneReg.test(12345)); // false正则的基础语法有几个必须记住的符号^表示开头$表示结尾\d表示数字{n}表示重复n次[abc]表示匹配a/b/c任意一个( )用于捕获分组。想要提取字符串里的某部分内容就用捕获组const str 订单编号A10086金额99元; const result str.match(/(A\d{5})/); console.log(result[1]); // A10086热词里的“javascript学习手册十正则表达式”其实可以总结成一句话正则就是一套字符匹配语法先掌握\d \w \s、量词、字符类、分组能覆盖九成以上的场景。遇到复杂的表达式建议用在线正则测试网站边写边测效率比纯靠自己脑补高得多。5.3 JSON与对象的互相转换浅拷贝与深拷贝JSONJavaScript Object Notation是一种轻量级的数据交换格式长得和JS对象很像但有两个关键区别JSON的键名必须用双引号JSON不能写注释、不能写函数。服务端返回数据时绝大多数情况下用的都是JSON字符串。两个核心方法const jsonString JSON.stringify(obj); // 对象 - JSON字符串 const newObj JSON.parse(jsonString); // JSON字符串 - 对象JSON.stringify在提交数据时用JSON.parse在解析后端返回时用。注意JSON.stringify有几个细节它会自动忽略值为undefined的函数和属性把NaN和Infinity转成null对循环引用的对象会直接抛错。JSON.parse(JSON.stringify(obj))是一个网上流传很广的“深拷贝”方案但它有坑对象里面有undefined、函数、Symbol、Date、正则的时候拷贝结果会出问题。热词里提到的“javascript对象”如果配合深拷贝场景我更推荐用structuredClone这个浏览器原生API它能正确处理更多类型或者直接引入lodash的cloneDeep。拷贝这件事一句话总结浅拷贝用展开运算符深拷贝在正式项目里用库别拿JSON当万能钥匙。6. 函数进阶this、闭包与错误处理6.1 函数的四种写法与this指向JavaScript里函数有多副面孔。最常见的四种写法// 1. 函数声明 function add(a, b) { return a b; } // 2. 函数表达式 const sub function(a, b) { return a - b; }; // 3. 箭头函数 const mul (a, b) a * b; // 4. 构造函数不推荐用于普通函数 function Person(name) { this.name name; } const p new Person(张三);函数声明和函数表达式的区别在于“提升”函数声明会被整体提升到作用域顶部所以你在声明之前调用它也能正常工作函数表达式不行。箭头函数则有几个不同的地方它不绑定自己的this、不能作为构造函数使用、没有arguments对象。热词里的“javascript函数”如果搜到的是“通过字符串调用函数”那一般是用括号后加参数的方式const fnName sayHello; window[fnName](); // 通过字符串动态调用全局函数这种方式在全局函数里能用但一旦代码被模块化封装全局函数往往访问不到。更可靠的做法是用一个显式的函数映射表而不是依赖window。6.2 闭包让函数记住自己的环境闭包是JavaScript里理解门槛较高的概念但你一旦懂了它很多代码模式自然就通了。一句话概括闭包就是一个函数连同它定义时所在作用域的结合体。看个例子function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2这里的count本应是createCounter内部的局部变量调用结束后就该被回收但因为返回的函数还在引用它所以它一直存活这就是闭包。闭包最常见的应用场景有两个一个是制造“私有变量”比如上面的计数器外部只能通过返回的函数来操作count另一个是缓存数据比如在一个函数里存一份计算结果避免重复计算。闭包的坑是内存泄漏风险如果一个闭包长期被全局变量引用它引用的整个作用域链都不会被回收在长生命周期的页面里容易造成内存占用上涨。所以我的建议是能用类或模块级变量就不用闭包必须用的时候确认闭包的生命周期不会超过业务需求。6.3 运行时报错与try/catch处理热词里有“javascript运行时报错”和“第3关javascript错误”这里专门聊聊错误处理。JavaScript的错误类型常用的有ReferenceError引用不存在的变量、TypeError类型错误比如调用不存在的方法、SyntaxError语法错误、RangeError超出有效范围、Error通用错误。处理运行时错误的语法是try...catch...finallytry { // 可能出错的代码 const data JSON.parse(input); } catch (err) { // 出错后执行 console.error(解析失败, err.message); } finally { // 不管成不成功都会执行 cleanup(); }try/catch能捕获同步代码里的错误但对异步代码里的错误无能为力除非你在异步函数内部就包好。Promise链上一般用.catch()async/await里就用try/catch包住await。这里我给一个非常实用的建议不要用空catch吞掉错误至少console.error一下否则线上出问题你连线索都没有。另外手动抛出错误用throw new Error(错误说明)。在设计API或封装函数时主动抛出有明确错误信息的异常比让程序跑着跑着莫名其妙返回一个undefined更好排查因为错误信息本身就是线索。7. 浏览器环境BOM、DOM与事件7.1 BOMwindow、location、navigator热词里有“javascript bom”全称是Browser Object Model浏览器对象模型核心对象是window它代表浏览器窗口本身。window下面挂着很多子对象最常用的几个window.location管理当前页面URL。location.href获取或跳转地址location.reload()刷新页面location.search拿到URL里的查询参数。window.navigator浏览器信息。navigator.userAgent判断浏览器类型navigator.language获取语言。window.history历史记录。history.back()、history.forward()、history.pushState()改变URL但不刷新页面。window.screen屏幕信息。屏幕宽高、可用区域。window还提供几个定时器方法setTimeout(fn, 延迟毫秒)、setInterval(fn, 间隔毫秒)、clearTimeout、clearInterval。定时器是异步执行的准确度并不高浏览器标签页切到后台时还可能被降频所以千万不要拿setInterval来做精确计时。做倒计时、动画帧率控制优先考虑requestAnimationFrame。7.2 DOM操作示例选择元素、改样式、触发事件DOMDocument Object Model是页面元素在JavaScript面前的模样。最常用的选择元素方法document.querySelector(.class-name); // 返回第一个匹配的元素 document.querySelectorAll(div); // 返回所有匹配元素的NodeList document.getElementById(id-name); // 按id获取 document.getElementsByClassName(class-name); // 按类名获取实时集合拿到元素之后可以修改文本、样式和属性const video document.querySelector(video); video.style.rotate -90deg; // 给视频旋转 video.play(); video.dispatchEvent(new Event(ended)); // 手动触发一个事件热词里那个document.querySelector(video).dispatchEvent(new Event(ended))其实就是这两行代码的组合先用querySelector选中页面的video元素再通过dispatchEvent手动派发一个ended事件。这种手法常被用来“模拟”用户操作或事件触发用于自动化脚本调试挺常见但要注意手动派发的事件和浏览器原生事件在isTrusted属性上不一样某些网站会通过这个属性判断事件是否来自真实用户所以这种模拟方式并不总是能绕过权限校验。注册事件监听最推荐的方式是addEventListenerconst btn document.querySelector(#btn); btn.addEventListener(click, function() { console.log(按钮被点击了); });相比老式的onclick fnaddEventListener的优势在于同一个元素可以挂多个事件处理函数而且可以指定捕获阶段还是冒泡阶段。事件冒泡是DOM事件默认的传播机制点击一个按钮事件会从被点中的元素逐级向上传播到根节点。利用冒泡可以实现“事件委托”——给父元素统一监听事件再通过event.target判断实际点中的子元素这在动态列表里特别省事。7.3 本地调试的三种常用方式调试JavaScript我建议新人至少掌握三种手段。第一种就是console.log最简单直接配合console.table打印数组、console.time计算耗时基本能解决一半问题。第二种是浏览器开发者工具的Sources源代码面板在代码行号上点击可以打“断点”代码执行到那里会暂停然后你可以一步一步看变量的变化、调用栈和当前作用域。比console.log强的地方在于你不用反复改代码加日志可以在运行时直接观察。第三种是Network网络面板专门用于排查接口请求问题。看请求是否发出、状态码、响应内容、请求耗时。前端联调时八成问题都能在Network面板里找到答案。提示很多人忽略的调试技巧——在代码里写debugger;语句程序执行到这一行时会自动暂停效果和在Sources面板打断点一样。当你出不了现场、只能让同事打开控制台时这个方法特别好用。8. 新手避坑常见报错与排查思路8.1 ReferenceError、TypeError等常见报错速查我见过太多新手被报错信息吓住了其实JavaScript报错信息已经告诉了你大部分线索。下面是我整理的一个速查表报错类型常见原因排查方向ReferenceError: xxx is not defined变量名拼错或没有声明检查变量声明和拼写TypeError: xxx is not a function把非函数当函数调用确认变量是不是函数TypeError: Cannot read properties of undefined访问了undefined的属性先判空再访问用可选链?.SyntaxError: Unexpected token语法错误漏括号或逗号看报错行号检查括号RangeError: Maximum call stack size exceeded无限递归检查递归出口条件NaN不该出现的数学运算或类型转换确认参与运算的变量是否是数字其中Cannot read properties of undefined是前端报错之王几乎每个前端开发者每天都在和它打交道。比如接口返回的对象里没有data字段你直接写res.data.list就会报这个错。最优雅的解法是可选链?.res?.data?.list如果中途是undefined整个表达式返回undefined不会抛错。8.2 代码不生效的6个排查方向经常有粉丝拿代码来问我“我写了为什么没反应”其实大部分问题都能通过下面这个清单逐项排查有没有语法错误。打开控制台如果有红色报错先从第一条开始解决。JS文件有没有加载。Network面板里看JS文件请求是否成功是不是404了。代码有没有执行。在代码顶部加console.log(start)看控制台有没有输出。选择器是否选中了元素。document.querySelector(...)返回的是null还是元素打印出来看一眼。执行时机是否太早。如果JS在DOM渲染之前执行querySelector会拿不到元素。把script放到/body之前或者用DOMContentLoaded事件包一层。作用域或拼写问题。变量名大小写写错了、函数内部引用了外部变量但外部变量不存在这些都能通过控制台报错定位。这个排查顺序基本能覆盖90%的“代码没生效”问题。养成先看控制台报错再动手的习惯比什么都重要。8.3 我给新人的学习路径建议如果你是从零开始认识JavaScript路线其实非常清晰先掌握基础语法变量、数据类型、运算符、条件、循环再学数组和对象两个核心数据结构然后是字符串、正则、JSON这三个文本处理工具接着理解函数和this、闭包再进入浏览器相关的内容DOM、BOM、事件最后学会用开发者工具调试。每一步都要配合练习。我的一个亲身体会是JavaScript这门语言看着很容易写着很容易踩坑但只要多写多调试很多概念会在一瞬间突然通了。比如this的指向你只看书可能永远觉得绕但当你在真实项目里因为this错了而排查了一个小时你就再也不会忘。所以哪怕只是每天在控制台里敲二十行代码都比只看不练强很多。这套基本功打扎实之后无论你想往前端框架Vue、React、后端Node.js、还是跨端小程序、Electron发展都会轻松不少。