JavaScript的模块机制

伴随着前端应用的复杂化, 将程序按照模块进行解耦和拆分逐渐成为了常态。 而JavaScript直到ES6才正式引入模块机制, 在这之前, 社区中涌现了CommonJS、AMD等多种模块化解决方案。

在JavaScript的语法规则中,脚本和模块是两种不同的概念:

脚本是由宿主环境引入并主动执行的JavaScript代码段,而模块则是在JavaScript代码中通过import引入进来,等待被调用的库.

相对于脚本而言,模块机制主要有语句,import声明和export声明组成。 但是不同规范和方案下的模块导入导出规则太难记了,本文就对ES6和CommonJS相应语法规则进行了梳理和总结。

ES6 Moldue语法

  • import 声明

ES6的import声明有两种使用方式:

  import "moduleA"; // 引入moduleA模块, 确保moduleA模块中代码被执行
  import a from "moduleA";  // 把模块moduleA默认的导出值放入当前变量 a

我们经常使用的是第二种带from关键字的,它有很多灵活的使用语法:

  import a from "moduleA"; // 引入moduleA中声明的默认导出对象
  import {b,c,d} from "moduleA"; // 引入moduleA中所导出的变量
  import {b as bbb,c,d} from "moduleA"; 
  import * as x from "moduleA"; // 将moduleA中所有导出的变量都放入当前x对象中
  • export 声明

export 命令声明了模块对外暴露的接口, 主要有三种使用方式:

  // export要导出的变量列表
  let a=1;
  let b=2;
  let c=3;
  export {a, b, c}

  // 直接在声明语句之前使用
  export let a=1;
  export function f(){}

  // 默认导出
  let a={}
  export default a

这里,export default的语义是,将a对象的值赋给一个名为default的变量并导出,以便引用方无需知道变量名就可以直接引入并使用:

  import bbb from "moduleA"; // 引入moduleA中声明的默认导出对象

这里有一点需要特别注意一下,export default导出的是一个变量值,后续模块中的变化和该值没有关系;而export导出的是变量本身,因此还会受到模块内部的控制和影响。

CommonJS语法

CommonJS是一种模块规范,而Node使用了CommonJS规范来对模块进行管理。

  • 导出

在CommonJS的模块导出语法有两种:

// 整体导出
let a={}
module.exports=a;// 导出对象a

// 导出多个变量
exports.b=1;
exports.b=2;

这里,module.exportsexports指向了同一个引用对象(var exports = module.exports),都指向了最终模块所导出的对象。 需要注意的是,由于模块最终导出的对象一定是module.exports所指向的对象,因此我们只能向exports上挂载变量,而不要去改变exports指向的对象。

  • 导入

CommonJS的模块导出相对来说比较简单:

let b=require("a") from 'moduleA'// 加载并读取moduleA所声明的module.exports对象

在Node中,require命令会读入并执行一个JavaScript模块文件,并根据该模块中所声明的module.exports在内存中创建一个导出对象(原对象的copy)。

对比

虽然CommonJS和ES6在语法颇为相似,但是在模块加载机制上却相去甚远。

我们上面提到,CommonJS在首次require一个模块的时候,会执行该模块代码并将所声明的module.exports对象缓存起来,以便下次使用。同时,这里所缓存的对象是原模块对象中的拷贝。

而ES6的import只是在编译阶段使用了一个引用来保存被导出对象,当代码执行期间才会根据引用地址去读取对应的值。因此,在外部对于导出变量的修改,可以影响到被引用模块内部。

References