
我把 JavaScript 从“脚本语言”这个标签讲起顺便把我这些年实际踩过的坑、总结下来的学习路径一起写了。如果你正在学 JS或者想快速搞清楚这门语言到底是怎么运作的这篇可以直接当参考。1. 为什么都2026年了还有人把JavaScript当“简单脚本语言”看说实话我刚入行的时候带我的师父让我先学 JavaScript我嘴上没说心里想的是一个脚本语言有什么好学的。那时候我对脚本语言的理解还停留在“跑批处理、写点自动化小工具”的层面觉得脚本语言嘛拿起来就能写写完就能跑不用编译没有类型约束顶多就是给网页加个弹窗、做点表单校验。这种认知让我在很长一段时间里严重低估了 JavaScript。直到后来我接手了一个中大型前端项目亲手把几千行的原生 JS 重构掉又一点点啃完闭包、原型链、事件循环、异步编程这些东西我才意识到——JavaScript 确实是一门脚本语言但“脚本语言”这个词根本不足以定义它。它更像是一门“寄生在浏览器里、却长成了整个互联网基础设施”的特殊语言。热搜词里有一堆“javascript基础”“javascript函数”“javascript事件”“javascript运行时报错”这说明现在依旧有大量新人在学 JS。但与此同时“javascript 框架或库是一组能轻松生成跨浏览器兼容的 javascript 代码的工具和函”这个长尾搜索词又说明很多人在学完基础之后会立刻困惑为什么有了 JavaScript我还要学 jQuery、Vue、React为什么我自己写的原生 JS 没法像框架那样几行代码就搞定一个组件这些困惑我自己都经历过。这篇文章我想做的就是把 JavaScript 这门语言的“脚本属性”和“工程属性”放到一起讲。对我来说学 JS 的正确路径不是先挑一个好用的框架埋头学而是先搞清楚脚本语言的工作方式、JS 在浏览器里的运行机制、函数和事件这两个核心概念以及为什么我们需要用框架来约束它。一句话总结我的观点JavaScript 的上手门槛确实低但它的天花板非常高而中间那条路靠搜索零散关键词是拼不完整的。2. 脚本语言到底是什么JS和Lua、Python脚本的“同”与“不同”聊 JavaScript绕不开“脚本语言”这个词。先把这个概念拆清楚后面所有内容都会好理解很多。2.1 脚本语言的核心特征解释执行 寄宿运行脚本语言通常有几个共同特征不需要经过编译成机器码而是由解释器逐行读取并执行它通常运行在一个宿主环境中而不是独立跑在操作系统上的进程它起步设计的目标是“把操作自动化掉”而不是构建大型复杂系统。JavaScript 完全符合这几个特征。它运行在浏览器这个宿主环境里由浏览器内置的 JS 引擎比如 Chrome 的 V8、Safari 的 JavaScriptCore、Firefox 的 SpiderMonkey逐行解释执行后来引入了 JIT 编译但这个后面说。它不需要你在本地装编译工具链打开浏览器写一个 HTML 文件里面塞一段 script 标签拖进去就能跑。这种特性对新手极其友好因为反馈速度极快改一行代码刷新一下页面就能看到结果。这是 JavaScript 能成为“最容易入门”的编程语言之一的原因。2.2 JS和Lua脚本的相似之处很多人搜“lua脚本语言”说明他们注意到了脚本语言之间的相似性。Lua 和 JavaScript 确实有不少共同点两者都是动态类型、都支持函数作为一等公民、都广泛用于嵌入到其他应用里扩展功能。Lua 最常见的场景是游戏脚本、Redis 脚本、Nginx 脚本它被嵌入到这些宿主程序中为程序提供可定制的逻辑扩展能力。JavaScript 干过一模一样的事。浏览器就是它的宿主网页就是它的扩展场景。甚至 Node.js 的出现也是把 JavaScript 从浏览器里“解放”出来让它寄生在服务端环境里于是 JS 既能写浏览器脚本也能写服务端脚本。但这两个语言走向了不同的进化路径Lua 的核心哲学是“小巧、可嵌入”全语言极简刻意不往大型语言方向发展JavaScript 则背靠 Web被巨大的生态需求推着走不停加特性从 ES5 到 ES6、ES7 一直演进到现在的 ESNext规模越来越大。2.3 JS和Python脚本的相似之处Python 也常被称为脚本语言它和 JS 在编程范式上很像动态类型、解释执行、大量依赖生态库。Python 的哲学是“尽量用一种明显的方式做事”JS 的哲学则是“给你足够多的方式做同一件事”——这是很多人觉得 JS 混乱的根源但同时也是 JS 灵活性的来源。举一个具体例子同样是遍历数组求和// 方式一传统 for 循环 let arr [1, 2, 3, 4, 5]; let sum 0; for (let i 0; i arr.length; i) { sum arr[i]; }// 方式二forEach 遍历 let sum 0; arr.forEach(item { sum item; });// 方式三reduce 归约 let sum arr.reduce((acc, item) acc item, 0);同样是求一个数组的和JS 至少有三四种写法而且每种写法的性能表现、可读性、适用场景都不一样。Python 里最自然的写法可能就那一两种。JavaScript 这种“多范式”的特点是它被诟病“灵活到混乱”的原因但也是它能够适配从简单脚本到大型应用的跨度最关键的能力。3. JS在浏览器里是怎么跑起来的引擎、解析过程、事件驱动理解 JavaScript 的运行机制至少能避免你写出“逻辑上正确但实际跑不动”的代码。我见过太多新手在搜索“javascript运行时报错”时其实问题根本不是语法不对而是没理解 JS 的执行模型。3.1 引擎的编译执行流程现在的 JS 引擎不会真的“逐行解释”整段代码那么原始。以 V8 为例大致流程是这样的解析阶段把源文件解析成抽象语法树AST这一步会检查语法错误。字节码生成AST 被编译成字节码。解释执行字节码先被解释器逐条执行。热点代码优化引擎会标记执行频率高的代码段交给 JIT 编译器编译成机器码下次执行直接走机器码速度大幅提升。所以准确来说现代 JavaScript 是“解释执行 即时编译”的混合模式。这也是为什么 JS 的性能虽然天花板不如 C但在日常业务场景下完全够用甚至 Node.js 处理高并发 I/O 场景时表现很好。3.2 事件驱动为什么JS必须“等”浏览器里的 JS 是事件驱动 单线程模型。所谓单线程意味着 JS 主线程一次只能执行一个任务。你可能会有疑问那为什么我同时发起网络请求、定时器、用户点击事件它们看起来像是在“同时”运行答案在任务队列和事件循环。我先用生活化的方式来解释你可以把 JS 主线程想象成一个只有一个窗口的办事大厅一次只能接待一个来办事的人。但大厅有一个叫“任务队列”的候客厅所有暂时处理不了的差事——比如网络请求还没回来、定时器还没到时间——都在候客厅里排队等着。当主线程把手头的事处理完就会去候客厅看有没有新的差事可以处理。这种模型最大的坑在于如果你在主线程里写了一个死循环或者一个特别耗时的同步任务整个事件循环会被卡住所有正在等待的响应都会超时。这就是为什么浏览器里的 JS 特别强调异步编程也为什么要引入 Promise、async/await 这些语法来解决“回调地狱”的问题。3.3 事件循环的经典案例我用一个具体的代码来说明这个执行顺序这是面试高频题也是实际开发中最容易困惑的地方console.log(1); // 同步立刻执行输出 1 setTimeout(() { console.log(2); // 定时器回调它会被放进宏任务队列 }, 0); Promise.resolve().then(() { console.log(3); // Promise 回调它会被放进微任务队列 }); console.log(4); // 同步立刻执行输出 4执行的结果不是 1、2、3、4而是 1、4、3、2。原因是同步代码优先执行完1 和 4之后事件循环先清空微任务队列Promise 的 3再去处理宏任务队列setTimeout 的 2。定时器设成 0 毫秒也不代表立刻执行它只是表示“最早能在 0 毫秒后被放进任务队列”。这个机制直接解释了为什么你写 setTimeout(() { ... }, 0) 去做某些事结果总是比 Promise.then 晚、比普通代码晚。很多运行时报错也和这个执行顺序有关——你以为某个数据已经赋值了其实异步回调还没跑到那个赋值代码。3.4 垃圾回收和闭包的关系JS 的内存管理是自动的主要靠垃圾回收机制GC来标记无用对象、释放内存。但这不意味着你可以完全不管内存。一个典型的坑是闭包导致的内存泄漏。闭包是 JS 里一个绕不开的概念同时也是“javascript基础”和“javascript函数”两个热门关键词背后最值得深挖的知识点。简单理解闭包就是“一个函数捕获了它外部作用域里变量”的能力。用代码说话function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2理论上createCounter 执行完毕之后它内部的 count 变量就失去了引用应该被 GC 回收。但因为返回的函数还持有对 count 的引用形成了闭包count 被保留下来每次调用 counter() 还能改写它。这个特性非常强大比如可以用来创建“私有变量”。但也容易踩坑如果你不小心在事件监听、定时器、回调函数中引用了大对象又没在合适的时机释放引用GC 就没法回收这些数据页面内存占用会不断上涨。我排查过线上一个页面越用越卡的 bug最后定位到是一个闭包引用了一个巨大的日志数组数组一直累积没被清理。4. 你逃不掉的三座大山函数、事件、DOM操作热搜词里“javascript函数”“javascript事件”“javascript 框架或库是一组能轻松生成跨浏览器兼容的 javascript 代码的工具和函”这三个词出现频率很高。我完全理解为什么——这三个恰恰是 JS 初学阶段最核心、也最容易陷入误区的部分。4.1 函数JS里函数不是“子程序”它是“一等公民”在很多语言里函数就是一段可复用的代码块。但 JS 里的函数地位高得多它是“一等公民”这意味着函数可以像变量一样被赋值、被传递、被作为参数传给另一个函数、被作为返回值返回。因为这个特性JS 才有回调函数、高阶函数、函数式编程这些玩法。我举一个真实业务里非常常见的场景一个接口返回了订单列表你需要过滤出金额大于100元的订单再按时间倒序排列。// 普通写法 function getHighValueOrders(orders) { let result []; for (let i 0; i orders.length; i) { if (orders[i].amount 100) { result.push(orders[i]); } } result.sort((a, b) b.createdAt - a.createdAt); return result; }// 用高阶函数组合 const getHighValueOrders (orders) orders.filter(order order.amount 100) .sort((a, b) b.createdAt - a.createdAt);第二种写法直接“像流水线一样”把函数组合起来可读性和维护性好很多。这就是函数作为一等公民带来的能力也是你不理解函数就永远没法理解框架源码的原因。再说一个实战中常见的坑函数声明提升和变量提升。用 function 关键字声明的函数会在代码执行前被“提升”所以你在声明之前调用也没问题但用 let 或 const 声明的函数表达式则不会提前调用会报 ReferenceError。foo(); // 正常执行输出 foo function foo() { console.log(foo); } bar(); // 报错Cannot access bar before initialization const bar function() { console.log(bar); };这个细节是“javascript运行时报错”最常见的来源之一值得记死。4.2 事件别在事件绑定上重复踩坑浏览器里的交互几乎全靠事件驱动。点击、输入、滚动、键盘按下都是事件。JS 的事件机制有两个核心概念事件冒泡和事件捕获。简单说当你点击页面上一个按钮时这个点击事件会先从 window 一路往下“捕获”到目标元素再从目标元素一路往上“冒泡”回 window。DOM 事件默认是在冒泡阶段处理监听器的。这个机制的实际价值在“事件委托”如果你有一个列表列表里有很多项你别给每一项都绑 click只给父容器绑一个 click通过事件对象的 target 属性判断是哪个子项被点了。document.querySelector(#list).addEventListener(click, (e) { const target e.target.closest(.item); if (!target) return; console.log(点击了第, target.dataset.index, 项); });这样写的优点是不管列表后面新增多少项都不用重新绑定事件性能更好因为只有一个监听器。这个技巧我几乎在每一个实际项目里都用到了。关于事件还有一个非常隐蔽的坑removeEventListener 必须传入与 addEventListener 时“完全相同的函数引用”才能解除监听。新手极容易写错成“看着像同一个函数其实每次都是新建的函数”导致监听器永远解不掉内存泄漏、重复触发接踵而至。// 错误示范移除不掉 element.addEventListener(click, function handler() { console.log(hi); }); element.removeEventListener(click, function handler() { console.log(hi); }); // 正确做法保存引用 const handler () console.log(hi); element.addEventListener(click, handler); element.removeEventListener(click, handler);4.3 DOM操作从原生API到那个旋转视频的技巧DOM文档对象模型是 JS 操作网页结构的桥梁。document.querySelector、createElement、appendChild 这些 API 你迟早要背熟。DOM 操作本身不难难的是性能问题——每一次 DOM 修改都可能引起浏览器重新计算布局、重新绘制页面大量频繁操作 DOM 会导致页面卡顿。实际开发的原则是把零散的 DOM 修改合并起来一次性更新。现代框架的本质其实就是在替你做这件事——用虚拟 DOM 的方式减少真实 DOM 操作次数。热搜词里有一条特别有意思“javascript:v document.queryselector(‘video’);v.style.rotate ‘-90deg’;v.s”。这看起来像是在某些平台上非常火的“把网页视频旋转90度再全屏”的小技巧很多人直接在浏览器地址栏里粘贴这种代码块来改网页布局。这类问题我收到过很多次。实际上这行代码利用了浏览器地址栏支持 javascript: 伪协议的特性它会执行后面的 JS 语句操作当前页面的 DOM。从技术上来说你可以在 console 里做同样的事const video document.querySelector(video); video.style.rotate -90deg; video.style.transform rotate(-90deg); // 兼容写法这类技巧能火恰好证明了一件事用 JS 操作 DOM 是“立即见效”的这正是脚本语言天然适合“小工具”属性的表现。但我想提醒一下类似写法只适合本地调试和个人学习。如果你把它用在生产环境的公共项目里需要考虑兼容性因为 style.rotate 是比较新的 CSS 属性老浏览器不认识。稳妥的做法是用传统的 transform 属性配合宽高调整video.style.transform rotate(-90deg); video.style.width 100vh; video.style.height 100vw;4.4 基础但常被忽略的字符串处理热搜词里还有“javascript学习手册八js函数”和“javascript学习手册九字符串”可见字符串在入门学习者心中的分量。字符串操作看着简单但它是前端处理数据时真正的日常。我提一个高频但容易写错的场景模板字符串。很多人已经习惯用反引号拼接字符串这很好。但要注意模板字符串里的 ${} 解析如果你 ES6 基础不牢很容易把花括号拼错或漏掉反引号导致整段语法报错。// 正确的模板字符串 const name 张三; console.log(你好${name}今天${new Date().toLocaleDateString()}); // String 常用方法 const str hello world; console.log(str.includes(world)); // true console.log(str.startsWith(hello)); // true console.log(str.split( ).length); // 2字符串拼接有两种思维差异初学时期直接“”拼接就够了但在大量动态文本注入场景模板字符串和 replace、split 方法的组合才是主力。有些人的代码里出现 XSS 漏洞也可能是在拼接 HTML 时没有转义特殊字符。这也是 JS 基础不扎实的具体表现之一。5. 我的亲身踩坑记录从“运行时报错”到彻底搞懂调试我见过太多人遇到 JavaScript 报错就慌然后把报错信息复制到搜索框里找答案再回来继续试。这种方式不能说没用但效率极低。我把自己经常遇到、也是搜索频率最高的几类报错整理一下并给出排查思路。5.1 TypeError: Cannot read property ‘xxx’ of undefined这是我职业生涯里见过最多的 JS 报错没有之一。本质原因就是你尝试读取的对象是 undefined 或 null。典型场景接口还没返回数据你就直接用了 res.data.list[0].name。因为网络请求是异步的渲染代码执行时数据还没到于是 undefined.list 直接炸了。解决方案分几个层次第一层空值保护。res?.data?.list?.[0]?.name可选链语法一种优雅的防御写法。第二层在接口层面拦截保证渲染前数据结构完整。第三层设计前端数据模型时宁可给初始空数组也不要给 undefined。我更推荐第二层和第三层结合。可选链能兜底但它会掩盖“数据接口变了”的信号让你更难发现接口层面的问题。正确做法是在渲染函数入口处做好数据结构校验一旦缺失就抛错或记录日志而不是一处一处用 ?. 打补丁。5.2 ReferenceError: xxx is not defined这个报错的本义是前面没有用 var、let、const 声明过这个变量。但实际工程里发生这个报错常见原因有三类拼写错误变量名多了个字母或少了几个字母。作用域问题变量在函数内部声明了你在外面想用它。函数内部声明的 let、const 不能被外部访问但函数内部可以访问全局变量。旧项目里的全局变量被覆盖比如你在某个脚本里声明了 let name在另一个脚本里声明了 let name前者把后者覆盖后者在某个回调里找不到变量。排查思路不是去搜报错文本而是先打开浏览器的 Sources 面板在报错行打一个断点重新加载页面观察程序执行到断点时的调用栈和变量值。我事后总结经验90% 的 ReferenceError 靠断点能直接看到问题因为你写的代码在哪个作用域、有哪些变量一目了然。5.3 “javascript运行时报错”这个热词背后的共性当你在搜索引擎输入“javascript运行时报错”你想找的其实是“这个终端提示是什么意思我该怎么修复”。但我发现很多人忽略了一个更高效的做法把浏览器的开发者工具当成第一排查工具而不是搜索引擎。开发者工具里有一个 Console 面板报错信息会打印出文件名、行号、列号和一个非常具体的错误描述。点一下文件名直接跳转到 Sources 源码位置。你还可以在代码里用 console.log、debugger 语句做临时排查。我个人的习惯是先看错误是什么类型。TypeError、ReferenceError、SyntaxError 的处理方向完全不同。再看出错的位置。绝大多数报错都能直接定位到文件、行号。利用断点观察报错那一行之前代码的上下文数据长什么样。最后才是搜索。你不是搜索“报错原文”而是搜索“报错类型 核心关键词 浏览器环境”。现在热词里甚至出现了“完美平台javascript”说明不少人正在某个具体业务平台上实践 JS。这类平台通常有自己的脚本生态代码通常跑在受限的宿主环境里遇到报错时你反而更要把基础语法和运行机制弄扎实否则很难排查。5.4 兼容性问题的坑为什么你本地能跑别人机器上白屏跨浏览器兼容是一个永恒的 JS 话题。很多新写的代码在 Chrome 里正常但在老 Safari、低版本微信内置浏览器里直接语法报错导致页面白屏。原因多是用了新语法比如可选链 ?.、空值合并 ??、Array.prototype.at()这些 API 在旧引擎里不存在。解决兼容性问题有两条路代码层面用 Babel 之类的编译工具把新语法转成 ES5 语法。工程层面引入 polyfill 或垫片为旧浏览器补充缺失的 API。我在实际项目里建议至少做到明确你面向的用户用什么浏览器版本搞一份浏览器兼容清单然后用构建工具来转译和注入 polyfill不要只在 Chrome 里测至少也要在 Safari 和微信内置浏览器里过一遍。6. JavaScript的进化与生态为什么最后大家都要学框架和库“javascript 框架或库是一组能轻松生成跨浏览器兼容的 javascript 代码的工具和函”这个搜索结果描述得非常准确它就是框架和库存在的本质。但我想把这句话展开讲透因为光知道定义不理解动机你在选型时依然会一头雾水。6.1 从原生JS到框架的必然性原生 JS 很自由但自由是有代价的。当你的页面只有几百行代码时手动操作 DOM、手动绑定事件完全没问题。但当项目膨胀到几万行、几十万行代码多个开发人员并行协作时原生 JS 的自由反而成了灾难全局变量容易被污染模块化全靠自觉。手动操作真实 DOM性能优化非常吃力而且容易出错。UI 状态和界面不一致改了这个忘了那个。框架和库本质上给你做了一整套“约束”它们规定了数据怎么管理、界面怎么渲染、事件怎么处理、模块怎么组织。React 的虚拟 DOM、Vue 的响应式数据、jQuery 的跨浏览器封装解决的都是这些工程化问题。我见过最典型的原生 JS 项目是一个后台管理系统一共一万多行代码全部写在几个大 script 标签里。每次加功能都要在茫茫函数里找到关联的变量和回调改一处就担心牵连十几个地方。后来我花了一周时间把它重构成一个 Vue 项目梳理完状态之后功能扩展轻松了不止一个量级。6.2 jQuery在“跨浏览器兼容”历史中的角色今天很多年轻人已经不知道 jQuery 当年有多重要了。在 IE 还大行其道的时代不同浏览器对事件处理、DOM 操作的 API 有明显差异写一行标准 JS 代码放到 IE 里可能直接不工作。jQuery 最大的贡献就是“抹平了浏览器差异”它内部做了大量兼容处理并提供简洁的 API。以事件绑定为例。原生 JS 的 addEventListener 在 IE8 时代不叫这个叫 attachEvent而 jQuery 提供的是统一的 .on() 方法。你在 jQuery 里写 .on(‘click’, handler)它内部帮你判断环境选哪种绑定方式。这个价值就是“跨浏览器兼容工具”最直观的体现。现代项目里jQuery 正在淡出主流但它的思想深深影响了后来所有工具。当你看到一个框架说“我们兼容到 IE11”“我们屏蔽了浏览器差异”你应当想起来 jQuery 走过的路线而不是觉得这是理所当然的。6.3 学习路径建议先原生后框架我的建议是别因为框架流行就直接跳进框架先花时间把原生 JS 的关键概念弄清楚。原因很简单框架只是在帮你处理“通用问题”但你的业务里总有框架解决不了的场景到时候你需要写原生逻辑来救火。而且面试、排查复杂问题、阅读源码时原生基础决定你能走多深。基于我自己的学习和带人经验我列了一条比较合理的学习路径掌握语法变量、数据类型、运算符、流程控制这是骨架。掌握函数声明、调用、参数、返回值、作用域、闭包这是灵魂。掌握对象和数组对象的属性操作、数组的增删改查、遍历方法。掌握 DOM 操作选择元素、读写属性、增删节点、修改样式。掌握事件机制事件绑定、事件冒泡捕获、事件委托。掌握异步回调函数、Promise、async/await、任务队列。接触工程化模块化、构建工具、包管理、代码规范。选一个框架深入理解它的核心思想和数据流。这条路径里有几个节点容易卡住一般是闭包和异步。卡住很正常不用焦虑我当年也是在工作中逐渐悟透的。关键是坚持看到运行结果不断用小例子验证理解而不是只看概念。6.4 下一步可以做什么从脚本到工具从工具到应用如果你已经掌握了基础我强烈建议做一个“把多个基础能力组合起来”的小项目。举个例子做一个本地的书签管理器支持分类筛选、搜索、标签、本地存储持久化。需要用到数组遍历、对象操作、事件委托、模板字符串、DOM 更新、localStorage 存取。做完这一个项目你会体验到原生 JS 在实际业务里的完整流程。进阶一点可以做一个组件化的轮播图、一个支持排序的表格、一个带本地保存的待办列表。做不同的项目本质上是在“曝光”你遗漏的基础知识帮你在缺漏处补齐。以我个人的体会JavaScript 学到最后你会发现它真正难的不是语法而是“思维方式的切换”从面向过程到面向对象从回调到 Promise从命令式操作 DOM 到声明式绑定数据。每一次切换都是一道坎但每迈过一道坎你能做的东西就多一个数量级。“脚本语言”只是它的起点而 JS 能走多远取决于你在这些思维转换上愿意投入多少。