commonJS和ES6模块化的区别

简介: 在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

一、简述区别


在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。 ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。


1.语法差异


CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。而ES6模块化在浏览器和node.js中都可以用。 语法上面,两者最明显的差异是,CommonJS 模块使用require()和module.exports,ES6 模块使用import和export。 在node.js使用模块化,需要将 CommonJS 脚本的后缀名都改成.cjs,ES6 模块采用.mjs后缀文件名。或者修改package.son里面的文件,type字段为module或commonjs。


2. 什么是运行时加载呢?


// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;


上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。


3. 什么是编译时加载或静态加载呢?


// ES6模块


import { stat, exists, readFile } from 'fs';


上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。


4.在 Node.js 中使用模块化,ES6 模块与 CommonJS 模块差异


它们有三个重大差异。


CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 注意:CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值 ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 原因:CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。 上述差异1举例:


commonjs
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
//lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。
//这是因为mod.counter是一个原始类型的值,会被缓存。
//除非写成一个函数,才能得到内部变动后的值。
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};


ES6 // lib.js


export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
//ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。


二、commonJS


CommonJS模块基本上包括两个基础的部分:一个取名为exports的自由变量,它包含模块希望提供给其他模块的对象,以及模块所需要的可以用来引入和导出其它模块的函数。 模块功能主要由两个命令构成require()和 module.exports属性


1. 基本用法


  • export


//module对象:在每一个模块中,module对象代表该模块自身。
//export属性:module对象的一个属性,它向外提供接口。
//node为每一个模块提供了一个exports变量(可以说是一个对象),指向 module.exports。这相当于每个模块中都有一句这样的命令 var exports = module.exports;
//定义需要导出的对象
function foobar(){
  this.foo = function(){
    console.log( "Hello foo" );
  }
  this.bar = function(){
    console.log( "Hello bar" );
  }
}
//将要导出的对象添加到expor对象中
exports.foobar = foobar;


  • require


var math=require('math');
//命名的变量名和引入的模块名不必相同
var Math=require('math');
//带路径和不带路径
//如果当前目录没有node_modules目录就必须要写路径
var add=require('./add.js')


三、ES6模块化


模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。


1.语法


export:
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};


//export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。上面代码输出变量foo,值为bar,500 毫秒之后变成baz。


export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
复制代码


export default


为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。


// export-default.js
export default function () {
  console.log('foo');
}
//其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
// export-default.js
//非匿名函数也是可以的
export default function foo() {
  console.log('foo');
}


import:


//import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。 //import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。 //如果import里面是一个对象,改写对象的属性是允许的。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。


import { firstName, lastName, year } from './profile.js';
function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}


import()动态加载


// 报错
if (x === 2) {
  import MyModual from './myModual';
}


上面代码中,引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,import和export命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。


const path = './' + fileName;
const myModual = require(path);


上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。因为require是运行时加载模块,import命令无法取代require的动态加载功能。 ES2020提案 引入import()函数,支持动态加载模块。

import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。


import(specifier)


//import()返回一个 Promise 对象。下面是一个例子。


const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });
复制代码


import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。


注意点:import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数 import()也可以用在 async 函数之中:


async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();


2.在浏览器中使用ES6模块


浏览器加载 ES6 模块,也使用


<script type="module" src="./foo.js"></script>


浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。


<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>


四、在 Node.js 中使用模块化


1. 在Node.js 中使用ES6模块


Node.js 要求 ES6 模块采用.mjs后缀文件名。


如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。


{
   "type": "module"
}


2. 在Node.js 中使用commonjs模块


Node.js 要求 ES6 模块采用.cjs后缀文件名。


如果不希望将后缀名改成.cjs,可以在项目的package.json文件中,指定type字段为commonjs。


{
   "type": "commonjs"
}


3.package.json 的 main 和exports字段


package.json文件有两个字段可以指定模块的入口文件:main和exports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。


main
{
  "type": "module",
  "main": "./src/index.js"
}


//上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。如果没有type字段,index.js就会被解释为 CommonJS 模块。


export


exports字段的优先级高于main字段。它有多种用法。


子目录别名


{
  "exports": {
    "./submodule": "./src/submodule.js"
  }
}


//上面的代码指定src/submodule.js别名为submodule,然后就可以从别名加载这个文件。


main 的别名


exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。


{
  "exports": {
    ".": "./main.js"
  }
}
// 等同于
{
  "exports": "./main.js"
}
//由于exports字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。
{
  "main": "./main-legacy.cjs",
  "exports": {
    ".": "./main-modern.cjs"
  }
}


条件加载


利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开--experimental-conditional-exports标志。


{
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs",
      "default": "./main.js"
    },
    //别名
    "./feature": "./lib/feature.js",
  }
}


相关文章
|
3月前
|
存储 缓存 JavaScript
什么是CommonJS模块规范
【8月更文挑战第12天】什么是CommonJS模块规范
37 2
|
20天前
|
JavaScript 前端开发 开发工具
ES6模块化
【10月更文挑战第11天】 ES6模块化是JavaScript中重要的代码组织方式,通过`import`和`export`实现模块间的功能导入与导出,具备独立作用域和静态特性,有效提升代码的可读性、可维护性和复用性,支持动态导入和循环依赖处理,是现代JS开发的关键技术。
|
20天前
|
JavaScript 前端开发
在实际开发中,如何选择使用 ES6 模块还是 CommonJS 模块?
【10月更文挑战第11天】 在选择 ES6 模块还是 CommonJS 模块时,需考虑项目需求、团队经验、运行环境、库兼容性、构建工具、代码可读性和性能等因素。ES6 模块适合大型项目,提供更好的模块管理和可读性;CommonJS 模块则适用于旧环境和特定库。
|
2月前
|
JavaScript
es6模块中使用commonjs定义的库
es6模块中使用commonjs定义的库
|
2月前
|
缓存 JavaScript 前端开发
JavaScript模块化开发:ES6模块与CommonJs的对比与应用
JavaScript模块化开发:ES6模块与CommonJs的对比与应用
30 2
|
3月前
|
JavaScript 前端开发 C++
CommonJS和ES6模块规范有何区别
【8月更文挑战第21天】
43 8
|
3月前
|
缓存 JavaScript 前端开发
|
6月前
|
缓存 JavaScript 前端开发
JavaScript模块化:CommonJS与ES Modules的对比与使用
【4月更文挑战第22天】本文探讨了JavaScript模块化的两种规范——CommonJS和ES Modules。CommonJS适用于Node.js,通过`require`同步加载模块,而ES Modules(ES6模块)用于前端,支持异步加载和静态导入导出。CommonJS有缓存,ES Modules无缓存。在选择时,Node.js环境常用CommonJS,但趋势正转向ES Modules,前端项目推荐使用ES Modules以利用其优化性能的优势。
|
6月前
|
JavaScript
JS模块化规范之ES6及UMD
JS模块化规范之ES6及UMD
86 3
|
6月前
|
JavaScript 前端开发
说说你对ES6模块化的理解?和commonJS的区别?
ES6模块化(也称为ES2015模块化)是ECMAScript 6中引入的一种模块化规范,用于在JavaScript中组织和管理代码。它提供了一种更优雅和强大的方式来定义、引入和导出模块。
76 0