当前端应用越来越复杂时,我们想要将代码分割成不同的模块,便于复用、按需加载等。
require 和 import 分别是不同模块化规范下引入模块的语句,下文将介绍这两种方式的不同之处。
1. 出现的时间、地点不同
年份 | 出处 | |
---|---|---|
require/exports | 2009 | CommonJS |
import/export | 2015 | ECMAScript2015(ES6) |
2. 不同端(客户端/服务器)的使用限制
require/exports | import/export | |
---|---|---|
Node.js | 所有版本 | Node 9.0+(启动需加上 flag –experimental-modules) Node 13.2+(直接启动) 如何使用? |
Chrome | 不支持 | 61+ |
Firefox | 不支持 | 60+ |
Safari | 不支持 | 10.1+ |
Edge | 不支持 | 16+ |
CommonJS
模块化方案 require/exports
是为服务器端开发设计的。服务器模块系统同步读取模块文件内容,编译执行后得到模块接口。(Node.js
是 CommonJS
规范的实现)。
在浏览器端,因为其异步加载脚本文件的特性,CommonJS
规范无法正常加载。所以出现了 RequireJS
、SeaJS
等(兼容 CommonJS
)为浏览器设计的模块化方案。
两种方案各有各的限制,需要注意以下几点:
原生浏览器不支持 require/imports,可使用支持 CommonJS 模块规范的
Browsersify
、webpack
等打包工具,它们会将 require/imports 转换成能在浏览器使用的代码。import/export 在浏览器中无法直接使用,我们需要在引入模块的
<script\>
元素上添加type="module
属性。即使
Node.js
13.2+ 可以通过修改文件后缀为 .mjs 来支持ES6
模块import/export
,但是 Node.js 官方不建议在正式环境使用。目前可以使用 babel 将ES6
的模块系统编译成CommonJS
规范(注意:语法一样,但具体实现还是 require/exports)。
3. require/exports 是运行时动态加载,import/export 是静态编译
CommonJS
加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。- 阮一峰
4. require/exports 输出的是一个值的拷贝,import/export 模块输出的是值的引用
require/exports
输出的是值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
import/export
模块输出的是值的引用。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
若文件引用的模块值改变,require 引入的模块值不会改变,而 import 引入的模块值会改变。
5. 用法不一致
(1). require/exports 的用法
1 | const fs = require('fs') |
exports 是对 module.exports
的引用,相当于
1 | exports = module.exports = {}; |
在不改变 exports
指向的情况下,使用 exports
和 module.exports
没有区别;如果将 exports
指向了其他对象,exports
改变不会改变模块输出值。示例如下:
1 | //utils.js |
(2). import/export 的写法
import fs from ‘fs’
1 | import {readFile} from 'fs' //从 fs 导入 readFile 模块 |
建议 1:建议明确列出我们要引用的内容。,使用 * 虽然很方便,但是不利于现代的构建工具检测未被使用的函数,影响代码优化。
同时需要注意
- 引入
export default
导出的模块不用加 {},引入非export default
导出的模块需要加 {}。
1 | import fileSystem, {readFile} from 'fs' |
一个文件只能导出一个
default
模块。在验证代码的时候遇到如下报错
1 | access to script from origin 'null' has been blocked by CORS policy |
前面提到过,浏览器引入模块的 <script>
元素要添加 type="module
属性,但 module 不支持 file:// 文件协议,只支持 HTTP 协议,所以本地需要使用 http-server 等本地网络服务器打开网页文件。
(3). import/export 不能对引入模块重新赋值/定义
当我尝试给 import 的模块重新赋值时
1 | import { e1 } from "./webUtils.js"; |
浏览器显示
Uncaught TypeError: Assignment to constant variable.
当我重新定义引用的模块
1 | import { e1 } from "./webUtils.js"; |
浏览器显示
(index):17 Uncaught SyntaxError: Identifier ‘e1’ has already been declared
(4). ES6 模块可以在 import 引用语句前使用模块,CommonJS 则需要先引用后使用
ES6 模块
1 | //webUtils.js |
1 | console.log(e); //export |
CommonJS
1 | //utils.js |
1 | console.log(a); |
程序报错
ReferenceError: a is not defined
(5). import/export 只能在模块顶层使用,不能在函数、判断语句等代码块之中引用;require/exports 可以。
1 | import fs from "./webUtils.js"; |
程序报错
Uncaught SyntaxError: Unexpected token ‘{‘
前面提到过 import/export 在代码静态解析阶段就会生成,不会去分析代码块里面的 import/export,所以程序报语法错误,而不是运行时错误。
6. 是否采用严格模式
严格模式是采用具有限制性 JavaScript 变体的一种方式
import/export
导出的模块默认调用严格模式。
1 | var fun = () => { |
require/exports
默认不使用严格模式,可以自定义是否使用严格模式。
例如
1 | exports.fun = () => { |
7. 其他模块化方法
动态导入
import(modulePath) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。
我们可以在代码中的任意位置动态地使用它。例如:
1 | import('/modules/my-module.js') //动态导入 |
建议: 请不要滥用动态导入 import()(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和 tree shaking 发挥作用