JS/TS 易踩坑知识点 --- 模块化与工程化类
一、循环依赖
1.1 什么是循环依赖
A 导入 B,B 又导入 A,形成循环引用。ES Module 中会导入 undefined 或未初始化的值。
// a.js
import { greet } from './b.js'
export function hello() {
console.log(greet()) // undefined!
}
// b.js
import { hello } from './a.js'
export function greet() {
return hello() // 可能报错或返回 undefined
}
1.2 解决方案
// ✅ 方案一:提取公共模块
// common.js
export function greet() { return 'Hello' }
export function hello() { return 'World' }
// a.js
import { greet } from './common.js'
export function sayHello() { console.log(greet()) }
// b.js
import { hello } from './common.js'
export function sayWorld() { console.log(hello()) }
// ✅ 方案二:使用动态 import(异步)
async function loadModule() {
const mod = await import('./module.js')
return mod.default
}
// ✅ 方案三:延迟导入
export function doSomething() {
import('./heavy-module.js').then(mod => {
mod.doHeavyWork()
})
}
二、命名导出 vs 默认导出混用
2.1 导出方式
// ✅ 命名导出(Named Export)
export const PI = 3.14159
export function add(a, b) { return a + b }
export class User { ... }
// ✅ 默认导出(Default Export)
export default function() { ... }
// 每个文件只能有一个 default export
// ✅ 重命名导出
export { PI as PI_VALUE, add as sum }
2.2 导入方式
// ✅ 命名导入
import { PI, add } from './math.js'
// ✅ 重命名导入
import { add as sum } from './math.js'
// ✅ 全部导入
import * as math from './math.js'
math.PI
math.add(1, 2)
// ✅ 默认导入
import defaultExport from './module.js'
// ✅ 默认导入 + 命名导入
import defaultExport, { named1, named2 } from './module.js'
// ❌ 混用导致取不到值
import { default as defaultExport } from './module.js' // 不推荐
2.3 常见错误
// ❌ 默认导出用命名导入方式
// module.js: export default function hello() {}
import { hello } from './module.js' // 报错!hello 是 undefined
// ✅ 正确导入默认导出
import hello from './module.js'
// ❌ 命名导出用默认导入方式
// module.js: export const PI = 3.14
import PI from './module.js' // PI 是模块对象,不是 3.14
// ✅ 正确导入命名导出
import { PI } from './module.js'
三、import 是静态的
3.1 不能在条件语句中使用 import
// ❌ 语法错误!
if (condition) {
import('./module.js') // SyntaxError
}
// ✅ 使用动态 import(返回 Promise)
async function loadModule() {
if (condition) {
const mod = await import('./module.js')
mod.default()
}
}
3.2 动态导入的使用场景
// ✅ 路由懒加载
const Home = () => import('./pages/Home.vue')
const About = () => import('./pages/About.vue')
// ✅ 条件加载
async function initAnalytics(provider) {
if (provider === 'ga') {
const ga = await import('./analytics/ga.js')
ga.init()
} else if (provider === 'gtm') {
const gtm = await import('./analytics/gtm.js')
gtm.init()
}
}
// ✅ 按需加载大模块
button.addEventListener('click', async () => {
const heavyModule = await import('./heavy-module.js')
heavyModule.process()
})
四、ES Module vs CommonJS
4.1 主要区别
// ES Module
export const PI = 3.14
export default function() {}
import foo from './foo.js'
// CommonJS
module.exports = { PI: 3.14 }
const foo = require('./foo.js')
4.2 关键区别
| 特性 |
ES Module |
CommonJS |
| 加载时机 |
编译时加载 |
运行时加载 |
| 模块引用 |
只读引用(live binding) |
值的拷贝 |
| 同步/异步 |
静态分析,可优化 |
require 是同步的 |
| this 指向 |
顶层 this 为 undefined |
顶层 this 为 module.exports |
| 环境支持 |
浏览器原生支持 |
Node.js 原生支持 |
4.3 CommonJS 的引用陷阱
// CommonJS: require 返回的是值的拷贝
// math.js
let count = 0
module.exports = { count, increment: () => count++ }
// app.js
const math = require('./math')
console.log(math.count) // 0
math.increment()
console.log(math.count) // 0(还是 0!因为 count 是拷贝)
// ES Module: 是 live binding,引用的是原始值
// math.mjs
export let count = 0
export function increment() { count++ }
// app.mjs
import { count, increment } from './math.mjs'
console.log(count) // 0
increment()
console.log(count) // 1(正确!)
五、打包与 Tree-shaking
5.1 Tree-shaking 前提条件
- 必须使用 ES Module 语法(
import/export)
- 代码不能产生副作用
- 打包工具支持(Webpack/Vite/Rollup 等)
5.2 标记无副作用
// package.json 中声明无副作用
{
"sideEffects": false
}
// 或者指定哪些文件有副作用
{
"sideEffects": ["./src/polyfill.js", "*.css"]
}
// 在文件顶部标记(Vite/Webpack 支持)
/* @__PURE__ */ import('./side-effect-module.js')
5.3 副作用示例
// ❌ 有副作用,会被 tree-shake 掉
// utils.js
console.log('模块被加载了') // 副作用
export function add(a, b) { return a + b }
// ✅ 无副作用
export function add(a, b) { return a + b }
// ✅ 导入有副作用的模块(确保执行)
import './polyfill.js' // 即使不导入任何导出也会执行
六、总结速查表
| 坑点 |
错误写法 |
正确写法 |
| 循环依赖 |
A import B, B import A |
提取公共模块或动态 import |
| 导出混用 |
默认导出用命名导入 |
区分 default 和 named import |
| 动态导入 |
if (x) import('./mod') |
const mod = await import('./mod') |
| CommonJS 引用 |
const {x} = require() 拷贝 |
ES Module 的 live binding |
| Tree-shaking |
文件有 console.log |
标记 sideEffects: false |
| 打包体积 |
全部打包 |
路由懒加载 + 动态 import |