欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

模块打包之CommonJS与ES6模块比较初探

程序员文章站 2022-06-13 15:56:05
...

Time: 20190920

模块是具有特定功能的组成单元,不同模块负责不同的工作,然后会以某种方式联系到一起,形成完整的程序逻辑。

CommonJS

CommonJS是2009年社区提出的,包含模块、文件、IO和控制台在内的一系列标准。

Node.js采用的是CommonJS标准的一部分,并在此基础上做了一些变化。

CommonJS标准,最初是为服务端设计,而有了Browserify后,可以将CommonJS模块打包为在浏览器下可以运行的单个文件。

导致的结果是:客户端的代码可以按照CommonJS标准来写。

Browserify是运行在Node.js环境下的模块打包工具。

此外,借助于包管理工具,前端开发开始流行用CommonJS标准了。

模块的定义

CommonJS中,每个文件是一个模块

通过script标签引入和封装成CommonJS模块的区别是什么?

前者:顶层作用域是全局作用域,变量声明和函数声明时会污染全局环境。
后者:形成属于模块自身的作用域,变量和函数仅自己可访问,外部不可见。

导出

导出是模块向外暴露自身的唯一方式。

CommonJS标准中,导出模块的语法是:

module.exports = {}

比如:

module.exports = {
	name: 'calculator', 
	add: function(a, b) {
		return a + b;
	}
}

这个module关键字从哪里来呢?可以理解为,CommonJS模块内部会存一个module对象用于存储当前模块的信息

module.exports用于指定模块对外暴露的内容,上面的代码导出了两个属性。

CommonJS也支持一种简单的导出语法:

exports.name = 'calculater';
exports.add = function(a, b) {
	return a + b;
}

可用下面的代码来理解:

var module = {
	exports: {},
}

var exports = module.exports;

因此,一个是赋值语法,一个是添加属性的方式,不可以直接对exports赋值,这会导致失效,即和module这个对象失去了关联。

混用情况辨析

exports.add = function(a, b) {
	return a + b;
}

module.exports = {
	name: "calculator"
}

上面最后只会导出一个name属性出来,add函数丢失。

还有一个小点需要注意的是,导出语句语法不表示代码的结尾,后面写的逻辑也会照常执行。

但一般比较好的习惯是,将导出语法写到文件末尾,这样的约定,代码比较好读。

导入

CommonJS采用的导入语法是:require

// calculator.js
module.exports = {
	add: function(a, b) {
		return a + b;
	}
}

// index.js
const cal = require('./calculator.js')
const sum = cal.add(2, 3)
console.log(sum) // 5

require模块的两种情况

  • 第一次加载,首先执行此模块,导出内容
  • 第二次加载,此模块的代码不会在执行,导出上次的结果

就是会用模块的module对象记录一个属性,module.loaded

require可以只执行,产生作用,而不用它返回的内容。

另外,require可以接收表达式,可动态指定模块的加载路径。

看到这里,我们应该可以想到import/export这种语法,是的,我们下面就进入ES6模块标准~

ES6模块标准

ES6是JavaScript发展的一个转折点。因为JS诞生之初就没有模块的概念。

ES6是JS官方委员会正式采纳的模块化标准。

// calculator.js
export default {
	name: 'calculator',
	add: function(a, b) {
		return a + b;
	}
}
// index.js
import calculator from './calculator.js' // .js可以省略
const sum = calculator.add(2,3)
console.log(sum) // 5

在ES6模块化标准中,也是将每个文件当作一个模块,每个模块有自己的作用域。

ES6默认采用的是严格模式,即默认有use strict的效果。

导出

ES6语法下,导出关键字是export,有两种形式:

  • 命名导出,可导出多个
  • 默认导出,只能导出一个

命名导出

export const name = "calculator";
export const add = function(a, b) { return a + b };

上面是分别导出,还可以用下面的写法,统一导出:

const name = 'calculator';
const add = function(a, b) { return a + b };
export { name, add };

两种写法效果相同。

另外,命名导出时可以用as重命名。

const name = 'calculator';
const add = function(a, b) { return a + b };
export { name, add as getSum };

默认导出

只能导出一个。

export default {
	name: "calculator",
	add: function(a, b) {
		return a + b;
	}
}

这种export default表示对外导出一个default变量,所以无需变量声明,直接导出了值。

// 导出字符串
export default 'This is String';

// 导出类
export default class {...}

// 导出匿名函数
export default function() {...}

导入

ES6语法下,用import来导入。

导入命名模块

// calculator.js
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };

// index.js
import { name, add } from './calculator.js';
add(2, 3); 

导入命名模块有两个要求:

  • import后面要跟一对大括号将导入的变量名包裹起来
  • 导入的变量名和导出的变量名完全一致

命名导出时可用as关键字进行重命名,导入时也可以。

import { name, add as getSum } from './calculator.js'

整体导入import * from xxx

将整体导入的结果赋予一个对象,从而导入的变量是该对象的属性。

导入默认模块

// calculator.js
export default {
	name: "calculator",
	add: function(a, b) { return a + b }
}

// index.js
import myCalculator from './calculator.js'
myCalculator.add(2, 3)

默认导出,在导入时,直接在import后面跟一个变量名,此变量名*指定,用于指代默认导出的值。

从形式上来说,加大括号包围的导入导出是命名的,裸的,不加大括号的是默认导入导出。

在React项目中,常见:

import React, { Component } from 'react';

就是二者的混用,即React是默认导出,而Component是命名导出的变量。

CommonJS和ES6的比较

本质区别:动态与静态

CommonJS对模块依赖的解决方式是动态的,ES6对模块依赖的解决是静态的

动与静: 动表示,模块的依赖关系在代码发生阶段。静表示模块的依赖关系建立在代码编译阶段。

在CommonJS中,require('./calculator.js')会将该模块中module.exports导出的结果加载进来,模块的路径可以动态加载,且支持传入表达式,还可以用代码逻辑判断是否加载模块等,非常灵活,是在模块被执行前判断不了的依赖关系。

所以说,在CommonJS中,模块的导入、导出在代码的运行阶段发生。

ES6模块则是声明式的写法,导入路径不能是表达式,且必须在模块的顶层作用域。

即,ES6是静态的模块写法,编译时就知道了模块的依赖的关系。

ES6模块相对于CommonJS的几个优点

  • 死代码的检测与排除
  • 模块变量的类型检查
  • 编译器优化

关于ES6和CommonJS标准的比较,下文继续展开。

END.