脚本放在<head>中执行时document.getElementById返回null,因为浏览器自上而下解析HTML,遇到<script>即暂停解析并执行,此时<body>尚未解析、DOM元素不存在;defer属性可确保外部脚本在DOM构建完成后按序执行,但内联脚本不支持defer,须用DOMContentLoaded事件包裹。

脚本在 <head> 里直接执行,document.getElementById 返回 null,不是代码写错了,是它根本没机会找到元素。
为什么 <script> 放在 <head> 里会找不到 DOM 元素
浏览器解析 HTML 是从上到下线性进行的。当它读到 <head> 里的 <script> 标签时,会立刻暂停 HTML 解析、下载(如果是外部脚本)、并执行脚本 —— 此时 <body> 甚至还没开始解析,所有页面元素都不存在于 DOM 中。
-
document.querySelector('#submit-btn')或document.getElementById('form')一定返回null - 对
null调用.addEventListener()或.innerHTML = ...就会抛出TypeError - 错误常静默发生:控制台报错,但页面看起来“只是没反应”
- 即使脚本体积很小、本地加载快,也不能依赖“它大概率能等到”,因为规范就是“遇到就执行”
defer 是最稳妥的通用解法
给 <script src="main.js"></script> 加上 defer 属性,浏览器就会把它变成“等 DOM 构建完再执行”,且保持多个 defer 脚本的声明顺序。
- 必须用于外部脚本(
src属性存在),内联脚本不支持defer - 脚本仍会并行下载,不阻塞 HTML 解析,但执行时机被严格推迟到
DOMContentLoaded前 - 比把脚本挪到
</body>前更可靠:后者依赖开发者手动维护位置,defer是声明式保证 - 兼容性好(IE9+),现代项目可放心用
哪些情况不能只靠 defer?
有些脚本本身不操作 DOM,但依赖其他脚本先执行(比如 jQuery 插件、或需要 window.$ 存在);或者你用了 async,它不保序也不等 DOM —— 这些时候 defer 不够用。
立即学习“前端免费学习笔记(深入)”;
- 依赖库未加载完成:确保 jQuery 的
<script src="jquery.js" defer></script>在插件脚本之前 - 用了
async:它会尽快执行,完全不管 DOM 状态,适合分析统计类无依赖脚本 - 需要精确控制时机:比如等图片加载完再初始化轮播图,就得用
load事件或IntersectionObserver,而不是只等 DOM - 内联脚本无法加
defer:只能包一层document.addEventListener('DOMContentLoaded', () => { ... })
别信“加个 alert 就好了”这种临时方案
有人发现把 alert('wait') 写在获取元素前,脚本突然能运行了 —— 这不是修复,是掩盖问题。用户关掉弹窗的瞬间,DOM 恰好构建完毕,纯属巧合。
-
alert阻塞主线程,但 HTML 解析不受影响,所以“多出来的等待时间”让 DOM 赶上了 - 网络稍慢、设备稍卡,这个“巧合”就失效,错误重现
- 用
setTimeout延迟几秒也一样不可靠:DOM 就绪时间不是固定值,而是取决于 HTML 规模和解析速度 - 真正要解决的,是让脚本执行逻辑和 DOM 生命周期对齐,不是靠猜时间
最易被忽略的一点:defer 只对有 src 的脚本生效;如果你把初始化逻辑写在 <script>...</script> 内联块里,它依然会在解析到时立刻执行 —— 即使它下面紧跟着你要操作的 <div id="app"></div>。



















