javaScript 中 import 和 require 究竟有何不同?

2,056 阅读5分钟

前言

在平时的前端项目中,我们都会书写 import 或者 require,但是我们真的又去思考过他们究竟有何不同呢?平时我们也会听过 AMD、CMD,那么他们又是什么意思呢?本文旨在回答这些问题,若有不对之处或者补充之处,还请评论区斧正。

require

require 语法属于 CommonJS规范,CommonJS 是服务器端模块的规范。我们熟悉的 Node.js 就是采用这种规范。 CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执⾏后⾯的操作。

同步执行

看到类似的文档我们都会看见文章强调 CommonJS 是服务器端模块的,因为 require 写法属于同步加载,也就是说会阻塞代码的执行,但是这个在服务器端并无影响,因为所有的文件都在硬盘上,模块的加载速度和硬盘的读写速度挂钩,实际耗费时间其实很短。

exports 和 module.exports

在我们刚学习 Node.js 的时候,一定会被 exports 和 module.exports 的区别产生一定的小疑问,其实他们的关系很简单,就是一个指向问题。

在 Node 中,每个模块内部都有一个自己的 module 对象,该 module 对象中,有一个成员叫:exports ,它也是一个对象,也就是说如果你需要对外导出成员,只需要把导出的成员挂载到 module.exports 中。

然而,module.exports与exports使用方法一致。但是,直接给exports赋值(exports="test"),不会生效。

因为每次导出接口成员的时候都通过 module.exports.xxx=xxx 的方式很麻烦,Node 为了简化你的操作,专门提供了一个变量:exports 等于 module.exports。但是,每次返回的是 module.exports。node每次都会做这样的操作:

var exports = module.exports
...
return module.exports

看到这里,相信你对这两者区别一定有了一定的了解了。总结:exports 和 module.exports 一开始指向同一个对象,但是直接给exports赋值,会改变exports的指向,但是最后模块的暴露还是依据的是 module.exports。

动态编译

在使用 require 后,第一次加载某个模块时,会缓存该模块的暴露结果,后续如果加载该模块就会从缓存中获取。并且 require 是运行时调用,所以 require 理论上可以运用在代码的任何地方,你甚至可以写在 if 判断语句之中。

// test.js
module.exports = {
    myStatus:'初始化'
}

// main.js
require('./test').myStatus = '修改过了'
const test = require('./test').myStatus
// 修改过了
console.log(test)

我们在 main.js 多次加载了 test.js,并且修改了缓存值。我们进行多次打印,发现值一样,说明并没有从新加载example模块, 而是从缓存中获取的该模块。

注意:require 即便通过解构进行引入,代码的实质是整体加载整个模块(即加载所有),生成一个对象,然后再从这个对象上面读取。

AMD 与 CMD

刚才我们提到了 CommonJS 引入模块由于是同步执行的,可以应用于服务端,但是客户端,限于网速等因素,如果同步加载,那么可能就造成“浏览器假死”的情况。社区中提出了 AMD 与 CMD 规范。

AMD和CMD都是为了解决浏览器端模块化问题而产生的,AMD规范对应的库函数有 Require.js,CMD规范是在国内发展起来的,对应的库函数有Sea.js。

简单来讲:就是在引入模块时,不会进行阻塞接下来代码的执行。我们可以在引入模块后,才进行某些代码的执行。

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  2. CMD推崇就近依赖,只有在用到某个模块的时候再去require

然而,后续两种规范愈发相像,都支持了对方的写法。它们最大的不同还是模块间的执行顺序:

  1. AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行。

  2. CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。

import

import 是由官方提出的模块加载方案,ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

静态编译

import 模块时只是生成引用,等到需要时才去取值,所以不存在缓存的问题。

本人的理解为:import 建立了文件模板之间的依赖关系,通过引用去获取,当你需要的时候,去相应模块去获取,所以不存在阻塞的现象。

值得注意的是:和其他语言语法类似,你应该把 import 放在文件头部。

动态加载

当然 import 无法做到 require 的动态加载。所以官网又提出了 import 函数来进行了补足。

import(`./xxxx.js`)
  .then(module => {
    ...
  })
  .catch(err => {
    ...
  });

语法

import 语法还是比较多,所以在这里单独说一下。

// 此步获取的是 default 对象
import xxx from '/xxx'
// 此步获取的是所有对象,放在 xxx 下
import * as xxx from '/xxx'
// 此步通过解构获取了相应的属性
import {xxx1, xxx2, xxx3} from '/xxx'

// 向外暴露一个属性值
export xxx
// 向外暴露
export {xxx1, xxx2, xxx3}
// 暴露出一个默认 default 对象
export default xxx

总结

  • CommonJS 模块输出的是一个值的复制(缓存),ES6模块输出的是值的引用(不缓存)
  • CommonJS 模块是运行时加载,ES6模块是静态编译(可以静态分析以及按需加载)