变量提升仅发生在脚本块的创建阶段,赋值在执行阶段,块间不共享执行时机;var声明可能为undefined,let/const受TDZ限制,function声明可跨块调用但需确保执行顺序。

多个<script>标签中var声明为什么有时是undefined</script>
因为变量提升只发生在“创建阶段”,而赋值发生在“执行阶段”——两个阶段被脚本加载顺序严格切割。浏览器按HTML顺序逐个解析每个 <script></script> 块,在每个块内完成自己的变量提升和执行,但块与块之间不共享执行时机。
常见错误现象:ReferenceError: a is not defined 或更隐蔽的 a 为 undefined,尤其当第二个脚本试图读取第一个脚本中 var a = 1 的值却失败时。
- 如果
<script>var a = 1;</script>在前,<script>console.log(a);</script>在后 → 输出1 - 如果顺序颠倒 → 第二个脚本执行时,第一个脚本还没加载,
a确实未声明,报ReferenceError - 若第一个脚本用
let a = 1,哪怕位置在前,第二个脚本仍会报错——let不提升,且存在暂时性死区(TDZ)
function声明能跨<script>调用,但要注意执行顺序</script>
function 声明会被完整提升到全局上下文的创建阶段,挂载到 window 上,所以只要它所在的 <script></script> 已加载完毕(哪怕还没执行到函数体),后续脚本就能调用它。但这不等于“安全”——执行时机仍可能出问题。
使用场景:工具函数提前声明、初始化逻辑分层(如先定义 init(),再在 DOM 就绪后调用)。
立即学习“前端免费学习笔记(深入)”;
- 前一个
<script></script>中写function init() { console.log('ready'); }→ 后一个可直接init() - 但如果前一个脚本里是
const init = () => {}或var init = function() {},则只提升变量名(init为undefined),调用会报TypeError - 即便函数已提升,若其内部依赖 DOM 元素,而调用发生在
解析前,仍会操作不到节点
动态插入的<script>不会延续原有全局上下文的执行流</script>
用 document.createElement('script') 插入的脚本,会触发一次新的、独立的全局执行上下文创建流程——它有自己的变量提升阶段,不继承之前脚本中 let/const 的值,也不感知前序脚本中尚未执行的赋值语句。
容易踩的坑:以为“插进去就等于接在上一个 <script></script> 后面”,结果发现变量突然变 undefined,或函数调用报错。
- 默认行为是
async = true,下载完立刻执行,不保证顺序;需显式设script.async = false才能同步阻塞(慎用) - 设
script.defer = true仅对同域有效,且只延迟到 DOM 解析完成,不解决跨脚本变量链断裂问题 - 更可靠的做法是封装成 Promise:
loadScript('a.js').then(() => loadScript('b.js')),并在b.js内部检查typeof ALib !== 'undefined'再继续
defer脚本里访问全局变量仍可能失败的三个原因
defer 只保证脚本在 DOM 解析完成后、按 HTML 顺序执行,但它不控制变量是否“已赋值”。很多开发者误以为 defer = “所有前面的脚本都跑完了”,其实不然。
性能影响:defer 脚本本身不阻塞渲染,但若它依赖的变量来自一个没加 defer 的前置脚本,而那个脚本又因网络慢或执行久导致延迟,就会出现竞态。
- 页面混用了
defer和非defer脚本:非defer的会立即执行并可能中断 DOM 解析,打乱预期时序 - 前置脚本里有异步操作(如
setTimeout、fetch初始化),变量实际赋值时间远晚于脚本执行结束 - 模块脚本(
type="module")自带隐式defer,但它运行在隔离的模块作用域,不会把变量挂到window,defer脚本读不到
真正关键的不是“脚本有没有执行”,而是“那个变量在哪一刻真正有了值”。靠位置和属性只能搭起骨架,最终还得靠显式检查、Promise 链或模块化边界来守住这条线。



















