12、为什么在 <script> 里写 export 会报错?

这不是"浏览器抽风",而是语言层级 的规则在起作用。把"脚本(script)"和"模块(module)"分清楚,你就能一次性搞定 export 报错、import 无效、return 放哪儿、use strict 生效范围等一堆疑难杂症。


先给结论(省流篇)

  • 不加 type="module"<script> 是"脚本文件(Script)" ,语法上不允许出现 import / export,所以直接报错
  • 加了 type="module"<script> 是"模块文件(Module)" ,才可以使用 import / exporttop-level await,并且天然处于严格模式。
  • 脚本可包含:语句模块可包含:import 声明、export 声明、语句(顺序无强制,但引擎会先处理导入/导出绑定)。
xml 复制代码
<!-- ❌ 会报错:export 只能出现在模块中 -->
<script>
  export const a = 1
</script>

<!-- ✅ 正确:这是模块 -->
<script type="module">
  export const a = 1
</script>

一、脚本 vs 模块:同是 JS,生而不同

ES6 之前,只有 脚本(Script) 一种源文件形态;ES6 引入 模块(Module) 后,语法层面分了家。

能力点 脚本(Script) 模块(Module)
语法支持 只允许普通语句 允许 import / export + 普通语句
this at top-level 指向全局对象(非严格模式) undefined(模块天然严格)
严格模式 可选:由 "use strict" 控制 始终严格模式
作用域 顶层污染全局 私有顶层作用域(不污染全局)
加载方式 <script> 直接执行,顺序依赖 <script type="module">importimport()
执行顺序 遇到就执行(可 defer/async 天然 defer,按依赖图静态解析、去重、只执行一次
top-level await 不支持 支持

标题之问的根因 :默认 <script> 被当成"脚本",而 export 是"模块语法",放错了"语法域"。


二、为什么 <script> 默认当"脚本"?

因为浏览器需要兼容 20+ 年的旧站点。如果不写 type,规范规定按脚本处理 。要启用模块,必须显式写:

xml 复制代码
<script type="module" src="/entry.js"></script>
  • type="module":告诉浏览器"按模块规范加载与解析"
  • 这会开启:静态依赖解析、严格模式、私有作用域、URL 解析、CORS 校验、Top-Level Await 支持 等一揽子能力

三、import & export:只在模块中合法

1)import 的 4 种常见形态

javascript 复制代码
import "mod"                      // 仅执行 mod 的顶层代码(不拿任何绑定)
import v from "mod"               // 默认导入
import { a as x, modify } from "mod" // 命名导入(带重命名)
import * as ns from "mod"         // 整体命名空间导入
// 组合写法
import d, { a as x } from "mod"
import d, * as ns from "mod"

Live Binding(活绑定) :命名导入拿到的不是"复制的值",而是只读引用,原模块更新会同步映射。

2)export 的 3 类写法

javascript 复制代码
// 声明式导出
export const a = 1
export function f() {}
export class C {}

// 批量导出"已存在的绑定"
const a = 1, b = 2
export { a, b as c }

// 默认导出(每个模块最多一个)
export default function () {}
export default class {}
// 或者导出一个表达式的值(与原变量"脱钩")
const obj = {}
export default obj // 之后再改 obj,不影响默认导出拿到的值

3)转发:export from

javascript 复制代码
export { a as x } from "./a.js"    // 直接把 a 转发出去
export * from "./a.js"             // 转发所有"命名导出"
export { default as D } from "./a.js"

四、一次看懂"为什么我这里还是错?"

场景 A:没写 type="module"

xml 复制代码
<script src="/index.js"></script>    <!-- index.js 里写了 export -->

现象 :控制台报 SyntaxError: Unexpected token 'export'
原因 :以脚本解析,遇到模块语法 → 语法错误
修正<script type="module" src="/index.js"></script>

场景 B:混用了脚本与模块的变量

xml 复制代码
<script>
  const secret = 42
</script>

<script type="module">
  console.log(window.secret) // undefined
  console.log(secret)        // ReferenceError: secret is not defined
</script>

原因 :模块顶层不再注入全局 ;脚本与模块顶层作用域隔离。
修正 :要么全部用模块,要么显式挂到 window.secret = 42

场景 C:想在脚本里"临时"用一下模块

脚本不能 import,但可以用动态导入(它本身是表达式,允许在脚本内使用):

xml 复制代码
<script>
  (async () => {
    const { add } = await import('/math.js') // ✅
    console.log(add(1, 2))
  })()
</script>

五、函数体、脚本、模块:谁能用什么语句?

三者都能装"语句列表",但模块多了导入导出,函数体多了 return

结构 语句 import / export return 严格模式
脚本(Script) ❌(顶层) 可选
模块(Module) ❌(顶层) 始终严格
函数体(Function Body) ❌(顶层) 由所在环境决定/"use strict"

另外还有四类函数体:普通、asyncgeneratorasync generator;决定能否使用 await / yield


六、预处理与指令序言:报错、提升,很多都在这里解释清楚

1)预处理(Hoisting-like 行为)

  • var:在脚本/模块/函数体级 提前声明(不管有没有执行到),不穿透函数体边界
  • function:在顶层 既声明也赋值(能提前使用);在块级(if 等)仅预声明,赋值在执行期
  • class在作用域内预留绑定,但不可访问(TDZ 抛错,行为更直觉)
javascript 复制代码
console.log(fn)   // function fn() {}
function fn() {}

console.log(C)    // ReferenceError(TDZ)
class C {}

2)指令序言(Directive Prologs)

  • 形如单个字符串字面量 的表达式,且必须位于最前
  • 用途:最常见是 "use strict" 开启严格模式
javascript 复制代码
"use strict";
function f(){ console.log(this) }
f.call(null) // 严格模式下为 null;非严格模式会变成全局对象

七、工程落地清单(团队可直接照抄)

1)页面里全量走模块

xml 复制代码
<!-- 推荐:所有入口都改为模块 -->
<script type="module" src="/main.js"></script>

2)将脚本迁移为模块的要点

  • 顶层不再往 window 挂变量(需要暴露就显式挂)
  • 跨文件通信改为 export / import 或事件/消息机制
  • 资源加载需遵守 同源/CORS ,相对路径要以模块 URL为基准
  • 可以用 top-level await(注意并发与依赖链性能)

3)混合场景兜底

  • 旧脚本中临时用模块:用 await import() 动态导入
  • 构建工具(Vite/Webpack/Rspack)里固定 module 入口,避免"脚本/模块"混用

八、易错点速查(收藏级)

  • <script> 默认按脚本 解析;必须 type="module" 才能写 import/export
  • 模块顶层是私有作用域严格模式 ,与全局隔离
  • 命名导入是活绑定,不是值拷贝:原模块更新,导入端也会变
  • export default objexport { obj as default } 行为不同前者导出"值",后者导出"绑定"
  • 脚本里用模块:await import('/foo.js')
  • 模块允许 Top-Level Await;脚本不允许
  • class 有 TDZ:预留绑定但不可提前用 ,与 function 不同

小作业

用你熟悉的 Babel/ESTree 工具,写一个小脚本,静态分析某个模块文件 ,打印出:

1)所有命名导出(含重命名)

2)是否存在默认导出

3)是否使用了 export from 转发


结语

export<script> 中报错,并不是浏览器"挑事",而是你把模块语法 写进了脚本语法域

分清 脚本 vs 模块 ,再配合预处理指令序言的理解,你会发现:很多"莫名其妙"的错误,其实都能在语法层面一次性解释清楚。

你们项目现在是"全面模块化"还是"脚本+模块混合"? 留言说说你遇到的最诡异一个模块加载问题,我来帮你现场定位并给出最小修复方案 👇

相关推荐
user94051035547172 小时前
Uniapp 3D 轮播图 轮播视频 可循环组件
前端
Junsen2 小时前
electron窗口层级与dock窗口列表
前端·electron
一个小潘桃鸭2 小时前
需求:el-upload加上文件上传进度
前端
梦醒繁华尽2 小时前
使用vue-element-plus-x完成AI问答对话,markdown展示Echarts展示
前端·javascript·vue.js
鹏多多3 小时前
关于React父组件调用子组件方法forwardRef的详解和案例
前端·javascript·react.js
吃饺子不吃馅3 小时前
AntV X6 核心插件帮你飞速创建画布
前端·css·svg
Ares-Wang3 小时前
Vue2 》》Vue3》》 Render函数 h
javascript
葡萄城技术团队3 小时前
SpreadJS 纯前端表格控件:破解中国式复杂报表技术文档
前端
Humbunklung3 小时前
C# 压缩解压文件的常用方法
前端·c#·压缩解压