JS/TS 易踩坑知识点 — 模块化与工程化类

JS/TS 易踩坑知识点 --- 模块化与工程化类

一、循环依赖

1.1 什么是循环依赖

A 导入 B,B 又导入 A,形成循环引用。ES Module 中会导入 undefined 或未初始化的值。

javascript 复制代码
// 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 解决方案

javascript 复制代码
// ✅ 方案一:提取公共模块
// 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 导出方式

javascript 复制代码
// ✅ 命名导出(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 导入方式

javascript 复制代码
// ✅ 命名导入
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 常见错误

javascript 复制代码
// ❌ 默认导出用命名导入方式
// 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

javascript 复制代码
// ❌ 语法错误!
if (condition) {
  import('./module.js') // SyntaxError
}

// ✅ 使用动态 import(返回 Promise)
async function loadModule() {
  if (condition) {
    const mod = await import('./module.js')
    mod.default()
  }
}

3.2 动态导入的使用场景

javascript 复制代码
// ✅ 路由懒加载
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 主要区别

javascript 复制代码
// 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 的引用陷阱

javascript 复制代码
// 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 标记无副作用

javascript 复制代码
// package.json 中声明无副作用
{
  "sideEffects": false
}

// 或者指定哪些文件有副作用
{
  "sideEffects": ["./src/polyfill.js", "*.css"]
}

// 在文件顶部标记(Vite/Webpack 支持)
/* @__PURE__ */ import('./side-effect-module.js')

5.3 副作用示例

javascript 复制代码
// ❌ 有副作用,会被 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
相关推荐
geovindu2 小时前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式
郝学胜-神的一滴3 小时前
C++11 工程级应用 09:告别无谓拷贝,解锁高性能移动语义
开发语言·数据结构·c++·vscode·软件工程·visual studio
MC皮蛋侠客4 小时前
OPC UA 系列(一):标准全景与 Python 最小闭环——让第一条设备数据流动起来
开发语言·python·opcua
默_笙4 小时前
🍕 AI 的嘴巴装了水管(上):从"等它说完"到"边说边听"的流式输出指南
前端·javascript
SamChan905 小时前
PDF翻译后的格式完整性校验:用Python自动比对译文与原文档的表格与段落结构
开发语言·python·ai·pdf·机器翻译
暖焰核心5 小时前
继承全解——继承、默认成员函数、切片、隐藏与虚继承
java·前端·javascript
君顾15 小时前
外卖CPS系统开发实战指南:从架构设计到部署全流程解析
java·开发语言·外卖
艾伦野鸽ggg5 小时前
前端异步请求的状态竞争(请求竞态)问题
前端·javascript·axios