过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系

于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

CommonJS

node

目前,只有node环境才支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node

官网地址:https://nodejs.org/zh-cn/

nodejs直接运行某个js文件,该文件被称之为入口文件

nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行

具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
  1. 路径必须以./../开头
  2. 如果模块文件后缀名为.js,可以省略后缀名
  3. require函数返回的是模块导出的内容
  1. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

有了CommonJS模块化,代码就会形成类似下面的结构:

nodejs希望支持es6_ES6

同时也解决了JS的两个问题

原理

// require中的伪代码
function require(modulePath){
   //1. 根据传递的模块路径,得到模块完整的绝对路径
   var moduleId = require.resolve(modulePath);
   //2. 判断缓存
   if(cache[moduleId]){
     return cache[moduleId];
   }
   //3. 真正运行模块代码的辅助函数
   function _require(exports, require, module, __filename, __dirname){
     // 目标模块的代码在这里
   }
   //4. 准备并运行辅助函数
   var module = {
      exports: {}
   };
   var exports = module.exports;
   var __filename = moduleId; // 得到模块文件的绝对路径
   var __dirname = ...; // 得到模块所在目录的绝对路径
   _require.call(exports, exports, _require, module, __filename, __dirname);
   //5. 缓存 module.exports
   cache[moduleId] = module.exports;
   //6. 返回 module.exports
   return module.exports;
}

// 根据传递的模块路径,得到模块完整的绝对路径
require.resolve = function(modulePath){
   // 略
}

面试题

下面的代码执行结果是什么?

// a.js
exports.a = 1;
module.exports.b = 2;
module.exports = function(){}
module.exports.c = 3;
exports.d = 4;
this.e = 5;
console(this === exports);
console(this === module.exports);
console(exports === module.exports);

// index.js
var a = arguments[1]("./a.js")
console.log(typeof a);
console.log(a.a, a.b, a.c, a.d, a.e);
console.log(arguments.length);

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现

ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入

浏览器使用以下方式引入一个ES6模块文件

<script src="JS文件" type="module">

标准和使用

  1. 模块的导出分为两种,基本导出(具名导出)默认导出
    可以将整个模块的导出想象成一个对象,基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
//导出结果:想象成一个对象
{
    a: xxx, //基本导出  具名导出  named exports
    b: xxx, //基本导出
    default: xxx, //默认导出
    c: xxx //基本导出
}

可以看出:

  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字

导出方式:

export var a = 1 //基本导出 a = 1
export var b = function(){} //基本导出 b = function(){}
export function method(){}  //基本导出 method = function(){}
var c = 3;
export {c} //基本导出 c = 3
export { c as temp } //基本导出 temp = 3

export default 3 //默认导出 default = 3
export default function(){} //默认导出 default = function(){}
export { c as default } //默认导出 default = 3

export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
  1. 模块的导入
    使用以下的代码导入模块
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
import c from "模块路径"  //相当于 import {default as c} from "模块路径"
import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
import * as obj from "模块路径" //将模块对象放入到变量obj中
import "模块路径" //不导入任何内容,仅执行一次模块

导入模块时,注意以下细节

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

面试题

问题:请对比一下CommonJS和ES Module

回答:

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。

ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。

CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。