广告

JavaScript中require与import的区别是什么?如何在实际项目中正确选择?

1. 核心概念与语言等级

1.1 CommonJS 与 ES Modules 的基本定位

在 JavaScript 的模块化体系中,require 属于 CommonJS,是一个同步加载的函数接口;import 则来自 ES Modules,属于一种对 ECMAScript 标准的原生模块化语法。两者的目标都是实现模块的导入与导出,但实现路径与生态环境不同。

了解这两者的差异,能帮助你在不同运行环境下做出合适的选择:现实场景比如 Node.js 的旧代码往往采用 CommonJS,而浏览器端与现代打包流程通常偏好 ES Modules

差异点核心在于:导入方式执行时机、以及 导出形式。下面的对比将帮助你快速把握要点。

1.2 执行模型与加载时机

require 在调用时才会执行,加载是同步完成的,这意味着在模块之间存在即时的依赖计算;import 具有静态分析的特点,加载与解析通常在编译或打包阶段完成,且支持 异步加载 的能力。

在某些构建流程中,这种静态性让打包器更容易进行 树摇优化分块代码分割,提高首屏加载效率;但这也要求你的代码遵循 ESM 的导出结构。为保持兼容性,许多项目选择在同一代码库中混合使用,但要用好桥接与互操作策略。

代码示例显示了两种导入的基本形式,便于快速对比:

// CommonJS
const mod = require('./mod');
console.log(mod.value);// ES Modules
import { value } from './mod.js';
console.log(value);

2. 语法与用法差异

2.1 导入方式的静态性与动态导入

ES Modules 体系中,import 是静态的,编译阶段就能确定依赖关系,便于静态分析、静态检查和提前优化;而 require 是一个 运行时函数,可以根据条件或运行时状态来决定加载的模块。

如果你需要在页面加载后按需加载某个模块,动态导入 提供了很好的方案,语法为 import('module'),返回一个 Promise,适用于代码分块和懒加载场景。

动态导入示例:

// 动态导入
import('lodash').then(({ default: _ }) => {console.log(_.sum([1,2,3]));
});

JavaScript中require与import的区别是什么?如何在实际项目中正确选择?

2.2 默认导出与命名导出

import 支持两种导出模式:默认导出命名导出require 通过 module.exportsexports 机制来暴露内容。这意味着在互操作时,常需要做一些兼容处理。

导出示例:

// 模块导出(ESM)
export default function foo() {}
export const bar = 42;// 模块导出(CommonJS)
module.exports = function foo() {}
module.exports.bar = 42;

3. 在实际项目中的适用场景

3.1 Node.js 环境与打包工具

在 Node.js 的最新版本中,ES Modules 已被广泛支持,但 CommonJS 仍是大量旧代码的基础。实际项目中,关注 package.json 的类型配置:如果 type 设置为 “module”,Node.js 会以 ESM 解析;否则默认使用 CommonJS。同时,打包工具如 webpackrollupesbuild(以及框架如 Next.js、Vite)都对两种模块制式提供兼容层。

从依赖角度看,CommonJS 支持直接引用本地或 npm 的 .js 文件,而 ESM 也允许引入 .mjs 或带 type: module 的包。混用场景常通过 interop 解决默认导出与命名导出之间的差异。

3.2 浏览器端与服务端的差异

浏览器原生对 ES Modules 提供支持,使用 type="module" 的 script 标签即可加载;而对较旧浏览器,需要通过打包、转译来实现兼容。服务端在使用 Node.js 时,如果仅采用 CommonJS,可直接使用 require;如果采用 ESM,则使用 import。两者在运行时也有不同的错误处理方式。

示例:在浏览器中原生导入 ES Module:

// 浏览器端

3.3 迁移策略与互操作

对于大型项目,通常采取分阶段迁移策略:优先将新的模块改写为 ESM,对现有的 CommonJS 进行兼容层封装,确保两者可共存。迁移时要关注 默认导出命名导出 的兼容性,以及打包时的分块策略。

兼容性示例:

// 将一个 CommonJS 模块改造成 ESModule 的过程需要有兼容导出
// before: const lib = require('lib');
import lib from 'lib'; // after
export { version } from 'lib';
// 若需要保留默认导出,可在 CommonJS 中设定 module.exports.default = module.exports;

4. 进阶特性与性能考量

4.1 按需加载与树摇优化

import 结合静态分析,打包器能实现 树摇优化,不被引用的导出会被舍弃,帮助减少打包体积;而 require 的运行时特性使静态树摇在某些场景中难以实现,因此在纯浏览器端 ESM 场景中,更易受益于 import 的静态性。

动态导入也是关键能力,动态 import 可以实现 按需加载,降低初始加载成本,但要注意加载时的错误处理与缓存策略。

代码示例:

// 代码分割示例:使用动态导入实现按需加载
async function loadFeature(){ const mod = await import('./feature.js'); mod.run(); }

4.2 错误处理与互操作性

动态导入的返回是一个 Promise,若模块加载失败需要通过 catch 处理;不同模块系统之间的互操作,常涉及到默认导出和命名导出的映射,避免出现 undefined 的情况。

错误示例:

import('./mod.js').then(m => { console.log(m.default); }).catch(err => { console.error('加载失败', err); });

5. 实际案例与代码对比

5.1 从 require 到 import 的逐步迁移示例

在你现有的 Node.js 项目中,初步替换一批模块的导入时,可以先对等价的 ESM 导入进行测试,以确保依赖关系和打包输出的一致性。逐步替换有助于减少风险并提升可维护性。

示例对比:旧版代码使用 require,新版本改为使用 import,同时调整导出形式以适应静态分析的要求。

对比代码:

// 旧版(CommonJS)
const http = require('http');
const server = http.createServer((req, res) => res.end('hello'));// 新版(ESM)
import http from 'http';
const server = http.createServer((req, res) => res.end('hello'));

5.2 混合项目的兼容方案

对于混合架构的项目,必要时通过桥接层实现 API 兼容,以便在不同入口中统一使用一种模块系统,降低维护成本。

示例桥接层:

// bridge.js
const lib = require('lib');
export default lib;

广告