前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库浏览器 script module 入口加载
JavaScript模块与语言机制

浏览器里用 script type="module" 加载模块要注意什么?

在 <script> 标签上加 type="module" 就能让浏览器按 ESM 规则加载脚本。模块脚本默认 defer,不阻塞 HTML 解析;自动启用严格模式;跨域加载需要服务器返回正确的 CORS 头。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#模块与语言机制#异步编程
先看核心答案读代码示例
理解线索

模块脚本和普通脚本的三个核心差异

  1. 执行时机模块脚本默认 defer,普通脚本阻塞解析或需手动加 defer
  2. 作用域模块脚本有自己的模块作用域,普通脚本的顶层变量挂在全局
  3. 跨域策略模块脚本请求必须通过 CORS 检查,普通脚本不需要

每个模块 URL 只会被下载和执行一次。即使多个 <script type="module"> 引用了同一个文件,也不会重复执行。

核心回答

先记住这个答案

在 HTML 里把 <script> 的 type 设成 "module",浏览器就会按 ESM 规则处理这个脚本——支持 import 和 export,自动启用严格模式,默认 defer 执行。浏览器会从入口模块开始,递归加载所有依赖,构建出一张模块图,全部加载完成后再按依赖顺序执行。

  • 模块脚本默认 defer,在 DOM 解析完成后按顺序执行
  • 自动严格模式,顶层 this 是 undefined
  • 跨域模块需要 CORS 头,否则加载失败
  • 用 nomodule 属性为老浏览器提供降级脚本

模块脚本的执行时机和加载行为

普通 <script> 标签遇到就执行,会阻塞 HTML 的解析。模块脚本不一样,它默认就是 defer 的——浏览器会在后台下载模块文件,等 HTML 解析完成后,再按它们在文档中出现的顺序依次执行。

模块脚本的基本写法HTML
<!-- 入口模块:浏览器会递归加载 app.js 的所有依赖 -->
<script type="module" src="./app.js"></script>

<!-- 内联模块:直接在标签里写 import -->
<script type="module">
  import { init } from './app.js'
  init()
</script>

<!-- nomodule:只有不支持 ESM 的老浏览器才会执行 -->
<script nomodule src="./legacy-bundle.js"></script>

nomodule 属性是给老浏览器准备的降级方案。支持 ESM 的浏览器看到 nomodule 会直接跳过,而老浏览器不认识 type="module" 所以不会执行模块脚本,两边互不干扰。 如果给模块脚本加上 async 属性,它会在加载完成后立刻执行,不再等待 DOM 解析完成,也不保证和其他模块脚本的执行顺序。适合那些不依赖 DOM 也不依赖其他模块脚本执行结果的独立模块。

和普通脚本的关键差异

模块脚本和普通脚本在作用域、严格模式、跨域这几个方面行为完全不同。

模块脚本有独立的模块作用域。在模块里用 const x = 1 声明的变量不会挂到 window 上,其他模块或脚本也访问不到。普通脚本的顶层变量全都是全局的,多个脚本之间容易互相污染。

模块脚本自动启用严格模式,不需要写 'use strict'。这意味着不能用 with,不能给未声明的变量赋值,this 在顶层是 undefined 而不是 window。很多人从普通脚本迁移到模块脚本时,因为顶层 this 变了而踩坑。

模块脚本请求必须通过 CORS 检查。即使是同一域名下的文件,如果从 CDN 域名加载模块,服务器必须返回 Access-Control-Allow-Origin 头。普通 <script> 没有这个限制。

模块的去重和缓存

浏览器加载模块时会维护一个模块映射表,每个 URL 只会被下载和执行一次。如果 a.js 和 b.js 都 import 了 utils.js,utils.js 只下载一次,两边拿到的是同一个模块实例。

这和普通 <script> 不同——写两个 <script src="same.js">,脚本会执行两次。模块的去重机制保证了模块内的副作用只发生一次,也保证了不同文件导入同一模块时拿到的是同一份数据。

需要注意的是,模块的去重是按完整 URL 匹配的。./utils.js 和 ./utils.js?v=2 是两个不同的模块,会各自下载和执行一次。

回答前,多想一步

容易答错的地方

以为模块脚本和普通脚本的 defer 行为完全一样
模块脚本默认 defer,但和普通脚本的 defer 有一个细微区别:模块脚本在加载自身和所有依赖都完成后才执行,而普通 defer 脚本只等自身加载完就行。如果模块图很深、依赖链很长,模块脚本的执行时机可能比预期更晚。
以为本地文件可以直接用 file:// 打开
模块脚本在 file:// 协议下会被浏览器拒绝加载,因为 file:// 不符合 CORS 策略。开发时必须起一个本地 HTTP 服务器,即使只是 npx serve . 也行。很多初学者双击 HTML 文件打开后发现 import 报错,原因就在这里。
试着用自己的话回答

面试官还会怎么问?

模块脚本里能用 document.write() 吗?

不能。模块脚本默认 defer 执行,在 DOM 解析完成后才运行,这时调用 document.write() 会清空整个页面。实际上,现代浏览器在 defer 或 async 脚本里调用 document.write() 时会直接忽略或在控制台警告。

<script type="module"> 加载的模块能不能被普通脚本访问到?

不能直接访问。模块脚本的变量在模块作用域里,不在全局。如果确实需要让普通脚本访问模块里的值,可以在模块里显式地挂到 window 上,比如 window.myLib = { ... }。但这样做就失去了模块化的意义,一般不推荐。

多个 <script type="module"> 之间怎么共享数据?

通过 import 同一个模块来共享。因为模块只执行一次,不同的入口模块 import 同一个数据模块时拿到的是同一份实例。在那个数据模块里维护状态,所有导入方看到的就是同一份数据,这其实就是单例模式的天然实现。

从一道题,走向一组知识

把知识连起来

模块与语言机制

export default 后面跟表达式和跟声明,有什么区别?

模块脚本里 export default 表达式的求值时机和声明导出不同,理解模块加载顺序后更容易分清。

模块与语言机制

给 import 进来的变量重新赋值会怎样?

浏览器加载模块后,导入的绑定是只读的。弄清模块加载机制有助于理解为什么不能在导入方重新赋值。

参考资料

  • JavaScript:模块与语言机制参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 模块脚本的执行时机和加载行为
  3. 和普通脚本的关键差异
  4. 模块的去重和缓存
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑