
很多想学前端的人第一次接触 JavaScript 时都会有一种错觉这个语言挺简单啊声明几个变量、写个循环、调一下 console.log 就能出结果比 C 语言、Java 那套类定义和编译流程轻松多了。但真到了面试或者写实际项目的时候才发现问题一个接一个var和let到底该用哪个为什么有人说var有坑明明写的是if (a 1)怎么a是字符串1也进了分支一个看起来没什么问题的函数调用时this突然指向了window后端返回的数据是 JSON前端怎么取里面的字段还老报Cannot read property of undefined还有那个让你抓狂的javascript:void(0)到底是什么意思有什么用这些问题本身不难难的是很多教程把它们放在一堆概念里讲了半天没讲到点子上。JavaScript 真正需要你花时间搞懂的不是语法本身而是它作为一门“弱类型、解释执行、单线程、原型继承”的语言在运行时是如何工作的。把这层逻辑理清楚了写代码才不再是“照着抄”。这篇文章不整虚的直接按一条“能落地、能跑通、能应对面试基础题”的路径把 JavaScript 的核心语法、DOM 操作、异步编程和排错思路过一遍。全程会有可复制的代码示例和实际运行结果说明。如果你想从零开始入门前端或者速成之后去看源码、刷面试题这篇文章可以作为你的第一份 JavaScript 速查手册。1. 为什么前端开发绕不开 JavaScript先给一个明确判断在很长一段时间里JavaScript 仍然是前端开发唯一不可替代的编程语言。你可能会说现在不是还有 TypeScript、WebAssembly 吗 TypeScript 确实越来越流行但它的本质是“JavaScript 的超集”最终编译出来、浏览器里跑的还是 JavaScript。WebAssembly 确实能在浏览器里做高性能计算但它主要负责的是图像处理、3D 渲染这类重计算场景做业务逻辑、操作界面、和后端打交道核心工作还是 JavaScript 在做。这意味着什么呢意味着无论你后面学 Vue、React还是用 Node.js 写后端底层都离不开 JavaScript。框架和工具可以迭代但语言本身的地位很难被撼动。从实际体验来看JavaScript 也是初学者入门编程性价比很高的语言写代码不需要复杂的开发环境。浏览器按 F12 打开控制台就能写所见即所得。语法风格接近 C 和 Java有编程基础的人上手很快。前端岗位需求量大面试考察点集中学习路径相对清晰。但也正因为它“容易跑起来”很多初学者会在两种情况下卡住。第一种只跟着教程复制代码没理解代码的运行顺序。比如下面这段代码function sayHi() { console.log(Hi); } sayHi(); console.log(Done);看起来很简单但如果你去理解 JavaScript 的“预解析”“事件循环”“调用栈”就会发现这段代码的执行过程比表面看到的复杂得多。如果你不理解后面遇到回调函数、Promise就很容易晕。第二种把 JavaScript 当成 Java 写大量使用 class 和复杂的继承关系。JavaScript 虽然支持class但它的继承模型是原型链和 Java 的类继承完全不是一回事。新手如果一上来就套 Java 的思维写 JS会写出很多别扭又难调试的代码。所以这篇文章的主线是先建立 JavaScript 运行直觉再抓核心语法最后落到能写页面、能调接口、能看懂报错的真实开发能力。2. 语言定位JavaScript 到底是一门什么样的语言在动手写代码之前先给 JavaScript 做一个“技术画像”。很多初学者一上来就背各种概念但概念如果不和实际运行表现结合很快就会忘。JavaScript 的几个关键特征如下特征含义对开发者的影响解释执行代码不经过编译由浏览器/Node.js 逐行执行一些错误只有在运行到那一行才暴露弱类型变量可以随时改变类型不同类型可以参与运算方便但也容易产生隐性 bug单线程同一时间只执行一个任务通过事件循环和异步来实现非阻塞动态性对象的属性和方法可以在运行时添加、删除灵活也容易误操作原型继承对象通过 prototype 链实现继承而非类继承和 Java/C 思维方式不同其中最重要的一个认知是JavaScript 是单线程的但通过事件循环机制处理异步任务。这里先不深入展开事件循环等你理解 setTimeout 和 Promise 之后再回头来看会觉得特别清晰。另外有一个新手必须纠正的误区JavaScript 和 Java 除了名字相似几乎没什么关系。JavaScript 最初叫 LiveScript后来为了蹭 Java 的热度改了名但它的语法和设计思路更接近函数式语言和 Self 这类原型语言。所以如果你之前有 Java 基础需要注意以下几点Java 有严格的类型系统JS 没有。Java 中类是模板JS 中对象是基本单位。Java 的方法重载靠参数类型区分JS 中同一函数传不同的东西完全合法。从学习策略上讲你不需要把 JavaScript 看成 Java 的简化版而要把它看成一种独立的语言按照它自己的规律去理解。3. 环境准备写 JavaScript 最少需要什么很多教程一上来就让你安装 Node.js、配置 Vue CLI结果新手光装环境就花了一小时热情全没了。这里先说结论学习 JavaScript 最开始的阶段你不需要安装任何额外的东西。打开 Chrome 浏览器按 F12或者按 CtrlShiftIMac 上是 CommandOptionI点击 Console 标签页就可以开始写代码了。例如输入console.log(hello javascript);回车之后控制台会输出hello javascript。如果只是讲语法浏览器控制台完全够用。但进入 DOM 操作和页面交互阶段建议你准备一个最小页面工程。推荐两种方式按个人喜好选择。3.1 方式一单个 HTML 文件最简单的做法是创建一个index.html文件把 JavaScript 写在script标签里!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleJavaScript 快速入门/title /head body h1 idappHello/h1 script const title document.getElementById(app); title.textContent Hello JavaScript; title.style.color #e5484d; /script /body /html然后把文件拖进浏览器就会看到一个红色字体的Hello JavaScript。如果说你的目标是入门这种方式最直接不需要安装任何工具。3.2 方式二VS Code Live Server如果你已经决定长期走前端这条路那么建议你尽早习惯使用 VS Code。安装 VS Code。在 VS Code 扩展市场搜索并安装 Live Server 插件。创建index.html、style.css、app.js三个文件。在index.html中通过script srcapp.js/script引入 JS 文件。右键点击index.html选择 Open with Live Server浏览器会自动打开一个本地服务地址。这种方式的好处是接近真实开发环境改代码后浏览器会热更新调试效率高。3.3 需要 Node.js 吗如果你以后要写 Vue、React或者做 Node 后端那 Node.js 是必须装的。但学习 JavaScript 基础语法阶段装不装都行。如果装了 Node.js你也可以在终端进入 Node 交互环境输入 JS 代码看结果。和浏览器控制台的区别不大。这里有一个要点无论用哪种方式都要学会正确地看报错信息。很多时候程序出问题不是语法不对而是某个变量不存在、某个属性拼错了。浏览器控制台会把错误信息直接打出来还会标出是哪一行——先看这一行再看变量名有没有拼错80% 的问题都能解决。4. 核心语法速通变量、数据类型与类型转换这一节是整篇文章的基础也是面试题里最容易挖坑的地方。4.1 变量声明var 和 let/const 到底怎么选JavaScript 声明变量有三种方式var a 1; let b 2; const c 3;新手最容易踩的坑是var。它的特点有以下三点存在变量提升hoisting可以在声明之前访问值是undefined。没有块级作用域if或for里声明的变量外面能访问。可以重复声明后面声明的会覆盖前面的。这里看一个面试非常爱考的题console.log(i); // 输出什么 var i 100;答案undefined而不是报错。因为var i被提升到了代码顶部但赋值语句没有被提升所以在执行console.log(i)的时候i 已经存在值为undefined。再看块级作用域问题if (true) { var name 张三; } console.log(name); // 张三可以访问到 if (true) { let age 18; } console.log(age); // 报错Uncaught ReferenceError: age is not defined因此日常开发中建议优先使用const只有当变量需要重新赋值时才用let不要使用var。这不只是约定而是从语言层面避免变量提升和作用域污染问题。4.2 数据类型七种类型和 typeof 的坑JavaScript 有七种数据类型其中六种是基本类型一种是引用类型类型说明number数字包括整数和小数string字符串boolean布尔值true 或 falseundefined未定义null空值symbol唯一值ES6 新增object对象包括数组、函数、普通对象等undefined和null看起来很像但含义完全不同undefined表示“声明了变量但没赋值”或者访问对象不存在的属性。null是一个手动赋值的空值表示“这个对象现在没有值”。typeof用来检测变量类型但有一个著名的坑console.log(typeof null); // object从逻辑上讲null是空值typeof 却返回object这是 JS 历史遗留 bug。有些面试官专门用这个考基本功你能解释清楚会加分不少。数组的 typeof 也是object所以判断一个变量是不是数组需要用Array.isArray()const arr [1, 2, 3]; console.log(typeof arr); // object console.log(Array.isArray(arr)); // true4.3 类型转换弱类型语言最隐蔽的坑JavaScript 的隐式类型转换经常让新手措手不及。最经典的一个例子console.log(1 2); // 12 console.log(1 - 2); // -1 console.log(1 1); // 11 console.log(1 1); // 11为什么这里和-的结果不一样因为在 JavaScript 中身兼两职既能做加法也能做字符串拼接。只要操作数中有一个是字符串就会优先执行拼接操作。而-没有字符串拼接的功能所以会尽量把字符串转成数字再计算。再看一个很常见的判断陷阱if (0) { console.log(进入了 if 分支); }实际输出是“进入了 if 分支”。因为字符串0是一个非空字符串在布尔转换中为false的字符串只有、0不在其中。新手容易觉得“0 就是 false”但在 JavaScript 中空字符串才是false0是true。从这个例子可以看出写 JavaScript 时尽量用严格相等不要用宽松相等。因为会触发隐式类型转换结果可能和直觉不一致。console.log(1 1); // true因为发生了类型转换 console.log(1 1); // false类型不同直接不相等实际项目中是更安全的比较方式也几乎是所有现代前端项目的默认规范。5. 函数与作用域从会写到写明白函数是 JavaScript 的核心前端面试基本绕不开。这里重点讲三类写法以及作用域和闭包的基础认知。5.1 三种函数写法对比第一种函数声明。function add(a, b) { return a b; }第二种函数表达式。const add function (a, b) { return a b; };第三种箭头函数ES6。const add (a, b) a b;三者的区别可以从以下几个方面理解函数声明有提升可以在定义之前调用函数表达式不行。箭头函数没有自己的this它的this指向外部作用域。箭头函数不能用作构造函数不能用new调用。箭头函数的arguments对象不可用但可以用 rest 参数替代。这里最值得注意的是this指向问题因为它和普通人的直觉很不一致。5.2 this 指向最常见的一个坑在 JavaScript 中this的指向取决于函数如何被调用而不是在哪里定义。这个说法比较抽象看例子。const person { name: 张三, greet: function () { console.log(你好 this.name); }, }; person.greet(); // 你好张三这里person.greet()是通过对象调用的所以this指向person能正常拿到name。但下面这样写就会有问题const person { name: 张三, greet: function () { console.log(你好 this.name); }, }; const fn person.greet; fn(); // 你好undefined因为fn()是全局调用没有通过对象调用this指向了全局对象浏览器中就是window而全局对象上并没有name属性。这里其实就是初学时候最常遇到的“为什么单独拿出来调用就出错了”问题的根源。如果改成箭头函数情况又不一样const person { name: 张三, greet: () { console.log(你好 this.name); }, }; person.greet(); // 你好undefined因为箭头函数没有自己的this它会从外层作用域继承。而这里的外层作用域是全局所以this.name还是undefined。关于this的建议是先记住最基础的规律不要背语法。你在日常开发中如果遇到事件回调里this不对最常用的方案是把外层this存到一个变量里或者改用箭头函数const person { name: 张三, greet: function () { setTimeout(function () { console.log(你好 this.name); }, 1000); }, }; person.greet(); // 你好undefined因为 setTimeout 的回调里 this 指向全局改成箭头函数const person { name: 张三, greet: function () { setTimeout(() { console.log(你好 this.name); }, 1000); }, }; person.greet(); // 你好张三箭头函数的this指向当前定义时的外层作用域也就是greet函数的this而greet是通过person调用的所以能正确拿到person.name。5.3 作用域与闭包作用域就是变量的可见范围。JavaScript 中的作用域有三种全局作用域。函数作用域。块级作用域let/const。闭包是一个让很多新手头疼的概念。一句话概括当内部函数引用了外部函数的变量并且内部函数被返回到了外部那么这些变量就不会被垃圾回收这就是闭包。看一个经典例子function createCounter() { let count 0; return function () { count; console.log(count); }; } const counter createCounter(); counter(); // 1 counter(); // 2 counter(); // 3外部变量count按理说在createCounter执行完后应该被回收但因为内部函数还在引用它所以它一直保留在内存中。每次调用counter()都能输出递增的值。闭包在实际项目中有很多应用比如防抖、节流、缓存数据、实现私有变量。初学阶段不用深挖但只要你能写出这个例子并且解释清楚过程面试基础题就过关了。6. 数组与对象日常开发真正的核心可以说前端开发大部分时间都在处理数组和对象。6.1 数组的常用高阶方法在 ES6 之后用 for 循环遍历数组的场景越来越少更常用的是map、filter、forEach、reduce这几个方法。先看一组数据const users [ { name: 张三, age: 22 }, { name: 李四, age: 18 }, { name: 王五, age: 30 }, ];需要把所有人的名字取出来用mapconst names users.map((user) user.name); console.log(names); // [张三, 李四, 王五]需要筛选出年龄大于等于 20 的人用filterconst adults users.filter((user) user.age 20); console.log(adults); // [ { name: 张三, age: 22 }, { name: 王五, age: 30 } ]需要计算所有人年龄总和用reduceconst totalAge users.reduce((sum, user) sum user.age, 0); console.log(totalAge); // 70这三个方法的高频场景各不相同但思路非常统一传入一个函数函数负责定义“每个元素怎么处理”方法本身负责遍历。forEach和map的区别值得留意。forEach只是遍历数组没有返回值map会返回一个新数组。如果只是想遍历并执行操作用forEach更合适如果想对原数组做变换并拿到新数组就必须用map。const arr [1, 2, 3]; const forEachResult arr.forEach((num) num * 2); console.log(forEachResult); // undefined const mapResult arr.map((num) num * 2); console.log(mapResult); // [2, 4, 6]6.2 对象访问与解构对象的基本访问方式有两种点语法和方括号语法。const person { name: 张三, age: 22, }; console.log(person.name); // 张三 console.log(person[age]); // 22方括号语法在处理动态字段名时非常有用const key name; console.log(person[key]); // 张三ES6 的解构语法可以大幅减少代码量const person { name: 张三, age: 22, city: 北京, }; // 传统写法 const name person.name; const city person.city; // 解构写法 const { name, city } person;数组也可以解构const colors [red, green, blue]; const [first, second] colors; console.log(first); // red console.log(second); // green6.3 JSON 序列化与解析前端和后端接口通信时几乎离不开 JSON。两个最常用的方法// 对象转 JSON 字符串 const person { name: 张三, age: 22 }; const jsonStr JSON.stringify(person); console.log(jsonStr); // {name:张三,age:22} // JSON 字符串转对象 const parseObj JSON.parse(jsonStr); console.log(parseObj.name); // 张三这里常见的报错是接口返回的数据不是合法 JSON在JSON.parse时会抛出异常。实际项目开发中建议先用console.log打印后端返回的原始数据确认格式后再解析。7. DOM 操作与事件处理让页面动起来JavaScript 和 HTML、CSS 配合就能实现页面动态交互。这部分重点讲 DOM 操作和事件绑定的核心套路。7.1 获取元素和修改内容最推荐的方法是使用document.querySelector因为它和 CSS 选择器一致语义清晰。// 通过 id 获取 const title document.querySelector(#title); // 通过 class 获取第一个匹配元素 const button document.querySelector(.btn); // 获取所有匹配元素 const items document.querySelectorAll(.item);修改内容const title document.querySelector(#title); title.textContent 新的标题; title.style.color #e5484d;如果想插入新的 HTML可以使用innerHTML但需要注意如果内容是用户输入的innerHTML存在 XSS 安全风险不建议直接拼接用户数据。7.2 创建元素和添加到页面比如需要动态生成一个列表const list document.querySelector(#list); const items [苹果, 香蕉, 橙子]; items.forEach((item) { const li document.createElement(li); li.textContent item; list.appendChild(li); });createElement创建元素textContent设置文本最后将元素通过appendChild挂载到页面上。这个模式几乎适用于所有“数据驱动渲染”的场景。7.3 事件绑定点击事件最常见的方式const button document.querySelector(#btn); button.addEventListener(click, function () { console.log(按钮被点击了); });事件委托是实际项目中经常用到的优化技巧。当页面上有很多动态生成的元素时给每个元素单独绑定事件效率不高更推荐把事件绑定在它们共同的父元素上const list document.querySelector(#list); list.addEventListener(click, function (event) { if (event.target.tagName LI) { console.log(点击了, event.target.textContent); } });这样即使后续往列表里新增了li也不需要重新绑定事件因为点击事件被父容器统一处理。需要注意的是如果 script 标签写在 HTML 的head中浏览器解析到 script 时后面的 DOM 还未生成直接查询元素会得到null。解决办法有三种把script标签放在/body之前。使用defer属性。在DOMContentLoaded事件后执行代码。document.addEventListener(DOMContentLoaded, function () { // 在这里操作 DOM const title document.querySelector(#title); title.textContent 页面已加载; });8. 异步编程从 setTimeout 到 async/await前端开发中网络请求、定时任务、文件读取都是异步操作。如果异步处理不当会出现“数据还没返回页面已经渲染了一部分”的情况。8.1 回调函数与回调地狱最早期的异步处理方式是使用回调函数function getUserData(callback) { setTimeout(() { const user { name: 张三, age: 22 }; callback(user); }, 1000); } getUserData((user) { console.log(user.name); });setTimeout本身是一个异步操作它不会阻塞后续代码执行。这个过程可以用一个例子直观感受console.log(开始); setTimeout(() { console.log(定时器执行); }, 1000); console.log(结束);执行顺序是开始 结束 定时器执行有些新手会以为程序是从上到下按顺序执行的所以“定时器执行”应该在“结束”之前。实际上setTimeout只是设置了一个延时任务执行到这一行时它不会等待 1 秒而是直接继续执行后面的代码。只有在当前同步任务执行完成后事件循环才会回来执行定时器回调。回调函数本身不难但一旦嵌套多层代码可读性会迅速下降getUserData((user) { getOrders(user.id, (orders) { getOrderDetail(orders[0].id, (detail) { console.log(detail); }); }); });这就是俗称的“回调地狱”。为了解决这个问题ES6 引入了 Promise。8.2 Promise 的方式Promise 是异步任务的一个容器它有三种状态pending进行中、fulfilled已成功、rejected已失败。function getUserData() { return new Promise((resolve, reject) { setTimeout(() { const user { name: 张三, age: 22 }; resolve(user); }, 1000); }); } getUserData() .then((user) { console.log(user.name); }) .catch((err) { console.error(请求失败, err); });then中拿到的是成功状态的数据catch中处理失败异常。多层嵌套改成链式调用getUserData() .then((user) getOrders(user.id)) .then((orders) getOrderDetail(orders[0].id)) .then((detail) { console.log(detail); }) .catch((err) { console.error(err); });8.3 async/await现代异步写法async/await是 Promise 之上的语法糖让异步代码写起来像同步代码。async function getData() { try { const user await getUserData(); const orders await getOrders(user.id); const detail await getOrderDetail(orders[0].id); console.log(detail); } catch (err) { console.error(获取数据失败, err); } } getData();使用时的两个关键约束await只能在async函数中使用。await后面的表达式通常返回一个 Promise。在实际项目中async/await是写异步逻辑的首选因为它能显著提升代码可读性也更容易做错误处理。9. JavaScript 运行时报错与 javascript:void(0) 问题入门阶段遇到报错非常正常但很多新手一看全是英文就慌了。这里列出最常见的几种运行时报错以及排查思路。9.1 常见运行时报错汇总报错信息可能原因排查方式Uncaught ReferenceError: xxx is not defined变量未声明或拼写错误检查变量名是否拼写正确是否在作用域内Cannot read property xxx of undefined对象为 undefined 时访问了其属性先打印对象本身确认数据结构是否正确xxx is not a function调用的类型不是函数检查变量是否被覆盖或赋值错误Unexpected token }语法错误多为缺少或多余括号检查代码块的括号是否匹配JSON.parse 报错传入的不是合法 JSON 字符串打印原始字符串确认格式其中“Cannot read property of undefined”是新手最容易碰到的后端一返回异常前端取数据时就出这个。排查的关键是先看undefined的到底是什么。比如const res { code: 500, data: null }; console.log(res.data.list); // 报错因为 res.data 是 null解决思路是加防御性判断const list res.data res.data.list ? res.data.list : [];9.2 javascript:void(0) 是什么搜索热词里经常出现javascript:void(0)。严格来说这不是报错而是一个合法的 JavaScript 表达式。void运算符的作用是对后面的表达式求值然后返回undefined。最常见的场景是a标签的href中a hrefjavascript:void(0); onclickhandleClick()点击/a它的作用是让链接点击之后不发生页面跳转只执行onclick中的点击逻辑。void(0)整体返回undefined浏览器不会跳转也不会刷新页面。在 React/Vue 框架开发中已经很少这样写了因为可以用e.preventDefault()或按钮元素替代。但如果看到老代码或者面试问起知道它是在“阻止默认行为”就足够了。9.3 调试工具的使用当控制台报错时建议按下面的顺序排查看错误信息的第一行定位报错文件和行号。在报错的代码之前加console.log打印关键变量的值。打开 Sources 面板在代码行左侧点击添加断点逐步执行。如果是网络请求返回的数据格式不对切换到 Network 面板查看响应内容。很多时候问题不是 JavaScript 语法而是“数据和预期不一致”。先打印数据再决定怎么改代码是最有效的排错方式。10. 从“会语法”到“能开发”最佳实践与后续进阶方向文章最后一部分从工程实践的角度给出一些贴合实际开发的经验。10.1 命名规范变量和函数使用小驼峰命名法比如getUserInfo、userList。常量使用全大写下划线分隔比如MAX_COUNT、API_BASE_URL。布尔值使用is、has、can前缀比如isLoading、hasPermission。类名和组件名使用大驼峰命名法。10.2 注释规范注释要说明“为什么”而不是“是什么”。// 不推荐 let num 10; // 定义 num 为 10 // 推荐 // 这里取 10 作为重试上限超过后放弃请求避免无限重试 const MAX_RETRY_COUNT 10;10.3 异常处理网络请求一定要加try/catch或.catch()不能假设后端每次都能正常返回。生产环境还需要在 catch 中给出用户可读的提示。try { const res await fetchData(); renderPage(res.data); } catch (err) { showToast(加载失败请稍后重试); }10.4 安全边界不要使用eval执行动态字符串。不要用innerHTML直接插入用户输入的内容优先使用textContent或原生createElement。任何需要权限的操作后端都必须做校验前端隐藏按钮只能算体验优化不能算安全防护。10.5 后续进阶路线当你把本文的基础语法和 DOM 操作都跑通之后下一步就按这个顺序深入学习 ES6 的新特性比如解构、模板字符串、展开运算符、模块化 import/export。理解事件循环Event Loop搞明白 setTimeout、Promise、请求之间的执行顺序。手写一些常用工具函数比如防抖、节流、深拷贝、数组去重。学习一个框架Vue 或 React 二选一。进公司看团队技术栈自己学习建议选 Vue 起步文档友好、上手快如果准备长期深耕前端React 的生态和心智模型也值得了解。学习构建工具至少要知道 Vite、Webpack 解决什么问题。看源码先从 Vue 的响应式原理或者知名开源项目的仓库结构入手不要一上来就啃大而全的源码。JavaScript 这条路入门只需要 2 小时但精通需要持续的项目积累。读完这篇文章之后你可以给自己设置一个训练目标不看教程用原生 JavaScript 写一个待办事项列表包括添加、删除、完成状态切换和本地存储。这道题如果能在两小时内独立完成说明你已经具备了继续进阶的基础。