
刚学 JavaScript 的时候我照着教程把示例代码一行行敲进编辑器打开浏览器却什么反应都没有Console 里干干净净按钮点了也没动静。折腾了半天才发现问题不在语法而在 JavaScript 代码编写位置搞错了。这门语言再强如果你把它放到了浏览器“不认”的地方或者放在了一个执行时还拿不到操作对象的位置写多少行都白搭。这篇内容只围绕一个主题JavaScript 的 3 种代码编写位置——行内式、内部式、外部式。我会把每种写法的原理、适用场景、优点和致命短板都拆开讲清楚还会补上 script 标签加载时机的知识以及新手最容易踩的几个坑和排查思路。适合刚接触 JS 的前端新手同样适合那些“照着写了很多代码但经常不生效”的同学。提示文中的例子建议全部自己敲一遍。看一遍和实际敲一遍学到的东西差得不是一点半点。1. 先搞懂“代码写在哪”为什么如此关键1.1 浏览器的解析方式决定了“位置”问题你在浏览器的地址栏输入一个网址或者直接双击一个本地 HTML 文件浏览器做的第一件事不是把页面“唰”地画出来而是从上到下、逐行读取这份 HTML 文本。这个读取过程叫 HTML 解析解析到文本内容会生成 DOM 节点解析到图片和样式表会发起网络请求而解析到 script 标签时浏览器的行为会突然变得特殊。如果遇到的 script 没有加任何修饰属性浏览器会立刻停下整个 HTML 解析过程先去下载这段 JS 代码再逐行执行执行完了才继续往下解析后面的 HTML。这意味着一个普通脚本是同步阻塞的它不仅阻塞页面的渲染也阻塞后续节点的生成。正因为这个机制脚本放哪里就显得格外重要。你把脚本放在head里它执行时 body 部分还没被解析出来脚本里想操作的所有页面元素都还不存在你把脚本放在 body 的末尾这时整个 DOM 基本解析完了脚本想拿哪个节点都能拿到。很多新手遇到的“明明代码没写错就是不生效”八成是位置不对。1.2 “不生效”的常见真相执行时机太早新手最常见的报错之一是document.getElementById(box)返回了null。页面里明明存在一个 id 为box的元素为什么取出来是空原因就是在脚本执行的那一刻浏览器还没来得及生成这个元素。你让一个还没出生的人回答现在几点他当然只能回答“不知道”。所以 JavaScript 的代码编写位置表面上是排版选择本质上是“运行时机”的选择。你希望脚本在什么时候执行就把它放在对应的位置。这是学习 JS 绕不开的第一道坎也是理解后面所有高级加载策略的基础。把这个逻辑想通了再去看defer、async、DOMContentLoaded这些概念会轻松很多。2. 三种 JavaScript 代码编写位置逐一拆解2.1 行内式直接写在 HTML 标签属性里行内式指的是把 JavaScript 代码直接写在 HTML 标签的事件属性中最常见的就是onclick。比如button onclickalert(你点击了我)点我/button页面加载后按钮被点击就会弹出提示框。这种写法在 demo 和小工具里很常见因为写起来最快不用新建文件不用额外结构标签属性里塞段代码就行了。但它的缺点同样明显。首先一旦逻辑变复杂代码会马上变得难读。一个按钮的处理逻辑可能有好几条语句把它们压缩在一个属性值里读起来非常痛苦。其次代码和 HTML 结构强耦合想要给另一个按钮复用这段逻辑只能复制粘贴改一处要挨个找。再有它对引号非常敏感外层属性用了双引号内层字符串一般要用单引号一旦字符串本身也包含引号很容易触发语法错误。还有一点容易被忽略行内式脚本会带来安全风险。如果把用户输入的内容直接拼接到onclick这类属性中等于给了别人在页面里执行任意代码的机会这就属于 XSS 攻击的范畴了。所以行内式只适合写个“一句话”级别的逻辑或者做本地教学演示正式项目里要尽量避免。2.2 内部式用 script 标签包起来放在页面里内部式指的是在一个script标签内直接写 JavaScript 代码通常放在 HTML 文件的body标签末尾。例如!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title内部式示例/title /head body div idapp你好/div script var btn document.getElementById(btn); btn.addEventListener(click, function () { alert(按钮被点击了); }); /script /body /html对新手来说这是最友好的方式。你不需要理解文件路径、不需要搭建服务器只需要在页面最后写个script标签代码就能跑。它天然规避了“DOM 没解析完”的问题因为执行时 body 内容已经全部生成。演示给同事看一个效果或者在本地做个单页小工具用内部式非常高效。但内部式也有自己的天花板。它的代码只能服务于当前页面别的页面想用同样的逻辑只能再复制一份进去。这样页面一多重复代码就疯狂膨胀改一个公共逻辑要一页一页翻。而且这些代码无法被浏览器单独缓存每次打开页面都要重新下载一遍影响加载速度。另外HTML 文件里混入大量 JS 代码会让页面结构变得臃肿不利于多人协作和维护。2.3 外部式把代码放进独立文件再引入外部式就是把 JavaScript 写在一个独立的.js文件里然后通过script src路径/script引入。比如项目目录长这样js-basics/ ├── index.html └── app.js在index.html里加一句script srcapp.js/script把代码都放到app.js里页面结构保持干净。这是真实项目里最主流、也是我强烈推荐新手尽早习惯的写法。它把“结构”和“行为”彻底分开了HTML 负责页面长什么样JS 负责页面怎么动。外部式的优点首先是可复用一个公共的helper.js可以被多个页面引用其次是可缓存浏览器首次加载某个.js文件后会把它缓存起来下次访问同一路径时直接读缓存减少网络流量再就是可维护每个文件职责清晰改日期格式化逻辑你大概知道要去找哪个文件而不是在一个巨大的 HTML 里搜索。它的门槛在于要理解文件路径。相对路径、绝对路径、上级目录../这些概念如果没有接触过第一次写外部式可能会遇到“文件加载不出来”的情况。不过这个门槛很低花十分钟实验一次就懂了性价比非常高。2.4 三种方式横向对比为了让你一眼看清区别我把三种方式的关键信息整理成下面的表格比较项行内式内部式外部式写法位置标签的事件属性里script标签内独立.js文件引入推荐放置位置尽量避免body 末尾head 配合 defer或 body 末尾复用能力几乎不可复用当前页面复用跨页面复用缓存支持无无有维护成本高中低典型场景教学演示、一句话逻辑单页小工具、学习练习正式项目、团队协作潜在问题引号嵌套、XSS 风险页面臃肿、重复代码路径写错、加载失败这张表不要求你现在背下来但要清楚一个结论行内式能不用就不用内部式适合学习和非常小的页面外部式才是一套可以长期成长的上层建筑。我们接着解释为什么外部式能“胜出”。3. 为什么正经项目几乎都选外部式3.1 结构、样式、行为三者分离的工程意义一个网页由三部分组成HTML 负责结构CSS 负责外观JavaScript 负责行为。这三种语言混在一起并不会出错但时间一长维护的人会崩溃。想象一份 HTML 里有几百行 CSS 写在style标签里又有几百行 JS 写在script标签里再夹杂着十几个onclick属性打开文件的第一感觉就是头晕。把代码拆开本质上是在拆“责任”。HTML 文件只描述内容结构样式交给独立的 CSS 文件行为交给独立的 JS 文件。这样每个人都可以在自己负责的文件上并行工作不互相干扰。改样式的人不需要碰逻辑代码改逻辑的人不需要在一堆标签里找自己的函数。这就是“关注点分离”也是外部式最底层的价值。你可能觉得这只是“文件组织方式”层面的区别但对工程化来说早期的文件组织方式决定了后面能不能顺利扩展。一开始就把 JS 堆进 HTML 的项目等到功能变多时再想拆出来成本远比你想象的高。3.2 浏览器缓存带来的真实性能收益很多人忽略了缓存这个点。假设你的网站有十个页面每个页面都要用到一段相同的公共函数。用内部式这段函数会被复制到十个 HTML 文件里用户每打开一个页面都要重新下载一次。就算这段函数只有 2KB十个页面就是 20KB 的重复流量而且浏览器没法帮你省下来。用外部式公共函数是一个独立的common.js文件只有一个路径。用户第一次访问时下载一次浏览器把它缓存到本地第二次访问其他页面再遇到同样的src浏览器直接命中缓存连请求都不发了。这是一个“一次下载处处使用”的效果对流量消耗和加载速度都有实打实的帮助。这里有个补充要点当你后续更新了common.js的内容浏览器仍然可能会使用旧缓存。所以工程里通常会给 JS 文件的 URL 加上版本号或哈希值比如common.js?v20250101。每次内容变化版本号也变浏览器才会重新下载新文件。这个技巧同样基于外部式才能生效。3.3 从多文件到模块化外部式是起点外部式文件除了可以引入普通脚本还可以配合 ES6 的模块化语法。你可以在一个 JS 文件里export一个函数在另一个文件里import它让代码像积木一样组装。现代前端框架比如 Vue、React它们的开发模式本质上也是“把功能拆成组件每个组件对应一个文件”再由构建工具打包合并。如果没有“把代码放进独立文件”的意识很难真正理解模块化和组件化。你可能会问三种写法里为什么要用外部式答案很简单因为它是一条通往工程化的路。行内式和内部式是死胡同你在这两条路上走再远也走不出“能用”到“好用”的转变。顺带提一句开头热词里那些“JavaScript 框架或库是一组能轻松生成跨浏览器兼容的 JavaScript 代码的工具和函数”本质上也是外部式思路的延伸框架提供了很多独立文件你通过引入和调用把它们组合起来框架再帮你处理不同浏览器之间的差异。你看地基还是那个 script 标签。4. 关键的加载顺序与运行时机script 放哪才算稳4.1 普通脚本的“同步阻塞”到底有什么影响我们已经知道普通script会阻塞 HTML 解析。这个机制带来的影响比想象中大。第一个影响是白屏。如果script在head里而脚本是一个需要下载很久的大文件那么浏览器会一直卡在下载脚本这一步页面内容完全出不来。用户看到的是一个长时间的空白页面体验极差。第二个影响是操作不到元素。脚本一旦在body之前执行后面所有尚未解析出来的 DOM 元素都不存在了。这也是为什么老教程会千叮万嘱script要放到body标签结束之前。放在末尾页面节点基本生成完毕大部分问题就自然消失了。这个方法到今天依然有效是新手最不容易出错的选择。你可以做一个小实验在head里写一段脚本执行alert刷新页面你会看到提示框弹出时页面还是空的点掉之后页面才会开始显示。这个现象直观展示了“阻塞”的含义。4.2 defer 与 async两把控制加载的钥匙光靠“把脚本放 body 末尾”可以解决大部分问题但还不够高效。想象你有一个需要全站用的公共库如果都放到 body 末尾浏览器必须等整个 HTML 解析完之后才开始下载这个库白白浪费了“解析 HTML 期间可以顺便下载 JS”的时间。于是有了defer和async两个属性它们都允许浏览器在下载外部 JS 时不阻塞 HTML 解析但执行时机不同。defer的意思是“延迟执行”脚本下载和 HTML 解析并行进行但脚本会等到文档解析完成后再执行并且多个defer脚本按照文档顺序依次执行。这非常像“把脚本放到 body 末尾”但下载时机提前了性能更好。async的意思是“异步执行”脚本下载完就立刻执行不等 HTML 解析完成也不管其他脚本的顺序。适合那种和页面本身没多大关系的独立脚本比如访问统计、广告投放。需要注意的是defer和async只对外部文件生效直接写在script标签里的内联代码这两个属性是不起作用的。4.3 新手可以直接照抄的安全姿势我给新手的建议很简单两种姿势都可以任选其一第一种把外部脚本放进head里加上defer属性head script srcapp.js defer/script /head第二种把脚本放到body末尾什么都不加body ... script srcapp.js/script /body这两种方式都能保证脚本在执行时DOM 已经完整。第一种的加载性能通常更好因为下载可以和 HTML 解析并行第二种的逻辑更直观适合刚学习时的练习。不要一上来就把async用在自己业务脚本上顺序乱了排查问题的难度会翻倍。5. 新手最容易踩的 5 个坑与排查思路5.1 行内式的引号地狱行内式最常见的报错是字符串引号嵌套失败。写onclickalert(你好)没问题但如果你写成onclickalert(你好)浏览器会把alert(和你好拆成两段代码直接语法报错。我自己踩过的坑是外层单引号内层又是单引号结果字符串被截断控制台报错信息还特别不明显。后来养成了一个习惯行内式里的字符串尽量用与属性外层引号不同的引号比如属性是双引号里面就用单引号如果字符串本身还包含英文单引号比如dont那就要用转义字符或者干脆放弃行内式改用内部式。逻辑稍微复杂一点就换写法不要硬撑这不是什么丢人的事。5.2 脚本放在 head 里操作 DOM 却拿不到元素这个坑太经典了。新手写好 HTML 结构把脚本放在head里然后document.getElementById取节点发现取回来是null。解决办法有四个方向把脚本放到body末尾给脚本加defer把操作逻辑放在window.onload回调里或者监听DOMContentLoaded事件。前两种是修改 script 的位置或属性后两种是延迟代码执行。我建议你优先学会前两种因为它们最接近真实项目的写法。window.onload和DOMContentLoaded要等页面全部资源加载完时机偏晚而且它们本来就属于“事件”范畴等理解了事件机制后再掌握也不迟。5.3 路径写错控制台一片红外部式刚入门时最常见问题就是src路径写错。文件叫app.js你写成srcapp.js但页面在js子目录里结果浏览器请求js/app.js404。排查这个问题的标准流程是打开开发者工具切到 Network 面板看那个红色的请求确认它请求的完整地址是什么再回到文件目录里对比一下问题马上能看出来。另外注意文件大小写很多服务器是区分大小写的App.js和app.js是不同文件。我给新手的建议是起步阶段就把所有 JS 文件和 HTML 文件放在同一个目录里用相对路径srcapp.js等熟悉了再尝试./、../、子目录这些更复杂的路径结构。5.4 同一个文件被引入两次有时候为了调试你会不小心在一个页面里引入两次同一个 JS 文件。如果文件里声明了let或const变量第二次引入时会出现Uncaught SyntaxError: Identifier x has already been declared的报错因为同一个命名空间里的变量不能重复声明。即使没有报错也可能出现函数被第二次覆盖的情况后面的脚本重新声明了一个同名函数把前面的覆盖了行为变得不符合预期。所以在页面里引入外部文件时要养成检查的习惯确认每个 JS 文件只出现一次避免无意识的重复引用。5.5 大小写和分号的隐藏陷阱JavaScript 是大小写敏感的语言。getElementById的B和D必须大写写成getElementById之外的任何变体都会报错。querySelector的S要大写addEventListener的L和E要大写。这些细节初学者经常漏掉报错时又因为错误信息不够直观找半天也找不到。分号的问题更微妙。JavaScript 有自动分号插入机制很多代码不写分号也能运行。但某些场景会踩坑比如function getValue() { return { ok: true } }这段代码里return后面换行了自动分号插入机制会在return后自动补一个分号后面那个对象永远不会被返回。新手遇到这个只会觉得玄学其实就是分号位置的问题。我在写外部文件时习惯统一加分号不依赖自动机制会减少一批莫名其妙的问题。6. 从“写完能用”到“工程化思维”6.1 小实验亲手验证三种写法的运行顺序理论看再多不如做个实验。我建议你新建一个文件夹比如js-basics在里面创建index.html和app.js。HTML 里放一个div和一个按钮然后在三个地方分别写入上报代码head里的行内属性、body末尾的内部式、外部文件的头部。打开浏览器的 Console你会看到它们的执行顺序。你会发现head里的行内式和外部文件里的代码在页面元素尚未完整时就已经执行了而body末尾的代码则能拿到所有节点。再给外部的 script 加上defer观察顺序发生变化。这个过程比读十篇文章都管用。还可以试试把一段操作DOM的代码放在head里控制台会直接告诉你那是个null或undefined。这时候不要慌尝试把它挪到body末尾问题消失。亲身经历过一次你以后写脚本位置就不容易再犯同样的错。6.2 从三种写法延伸出去的学习路线三种代码编写位置只是 JavaScript 的起点。等你能熟练用外部式组织代码后接下来可以按这个顺序继续扩展先学函数声明和变量作用域搞明白var、let、const的区别再用typeof、Array.isArray、Object.prototype.toString处理常见的“JS 判断数据类型”问题接着学字符串和数字处理的常用方法比如toFixed(2)保留两位小数、数组的增删改查然后尝试OC 和 JavaScript 互相调用这类跨端交互场景理解 JS 不止能在浏览器里运行。你会发现无论你最终去做 Canvas 绘图、写全屏页面特效、接第三方日历库如fullcalendar还是在页面里处理加载进度条这类交互落脚点都是“写在哪里、什么时候执行”。这个基础打牢了后面所有特效、框架、库带来的复杂度都会变得可控。我个人在做前端这些年最大的体会就是大多数“奇怪”的问题到最后都能追溯到加载时机和代码位置。与其一上来就背各种高级 API不如先把 script 放在对的地方理解浏览器为什么这么执行。你写下的每一段 JavaScript都需要一个正确的位置来承载它。位置对了运行自然就顺了。最后再分享一个小技巧写完代码先别急着刷新养成打开开发者工具看 Console 和 Network 的习惯。Console 能告诉你代码哪里报错Network 能告诉你文件是否加载成功、加载了多久。把这两个面板用惯了你排查 JavaScript 问题的速度会比身边同学快一个量级。