CommonJS和ESModule

一、JS模块化的起源

在早期,JS 是没有 模块化 这一概念的,都是通过 script 标签引入 js 文件代码。当然,这对于写一些简单的需求没有什么问题,但当我们的项目越来越庞大时,我们引入的 js 文件就会越多,这时就会出现以下问题:

  • js文件作用域都是顶层,这会造成变量污染
  • js文件多,变得不好维护
  • js文件依赖问题,稍微不注意顺序引入错,代码全报错

为了解决以上问题 JavaScript 社区出现了 CommonJSCommonJS 是一种模块化的规范,包括现在的NodeJS 里面也采用了部分 CommonJS 语法在里面。那么在后来 ES6 也正式加入了 ES Module 模块。

二、CommonJS

CommonJS 是一种模块化的规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJS,后来为了体现它的广泛性,修改为 CommonJS,平时我们也会简称为 CJS。

Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发。

  • Node 是CommonJS在服务器端一个具有代表性的实现;
  • Browserify 是CommonJS在浏览器中的一种实现;
  • webpack 打包工具具备对CommonJS的支持和转换。

CommonJS规范中的核心变量:exportsmodule.exportsrequire

exportsmodule.exports 负责对模块中的内容进行导出;

require 函数负责帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

2.1 exports 导出

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会被导出。

不能使用:exports = xxx,在导入后只能拿到一个 {}

因为 exports 在默认情况下是指向 module.exports 对象的引用,如果为 exports 赋值了,那么也就是说 exports 不再指向 module.exports 所指的对象的地址,而我们向外共享成员的最终结果是 module.exports 所指的对象,如此便会导致错误。

牢记一条原则:无论是使用 exports 还是 module.exports,最终导出的结果都是以 module.exports 所指向的对象为准。

/** bar.js */
exports.name = "张三"
exports.age = 18
exports.sayHello = function() {
  console.log("Hello!")
}

/** main.js */
const bar = require('./bar')
console.log(bar) // { name: '张三', age: 18, sayHello: [Function: sayHello] }

2.2 module.exports 导出

module.exports 和 exports 除了在用法以外,没有区别!

exports 和 module.exports 指向的是同一个对象(为了不混淆,可以理解为 exports 是 module.exports 对象地址的一个引用,exports 本质是一个变量)

/** bar.js */
// 导出一个对象
module.exports = {
  name: '张三',
  age: 18,
  sex: '男',
  height: 1.88
}

// 导出任意值
module.exports.name = '张三'
module.exports.age = undefined
module.exports.sex = null

/** main.js */
const bar = require('./bar')
console.log(bar) // { name: '张三', age: undefined, sex: null, height: 1.88 }

2.3 混合导出

CommonJS 支持混合导出,exportsmodule.exports 可以同时使用,但需要注意覆盖问题的发生。

/** bar.js */
exports.name = "张三"
exports.age = 18
module.exports = {
  name: '李四',
  age: 24,
}

/** main.js */
const bar = require('./bar')
console.log(bar) // { name: '李四', age: 24 }

2.4 重复导入

模块在被第一次引入时,模块中的js代码会被运行一次

模块被多次引入时,会缓存,最终只加载(运行)一次

/** bar.js */
exports.name = '张三'
console.log('bar.js 运行了~')

/** main.js */
require('./bar') // bar.js 运行了~
require('./bar') // 没有输出 因为没有运行

2.5 动态导入

CommonJS 是运行时加载,所以其支持动态导入,也就是支持在js代码中使用 require 语法。

/** bar.js */
exports.name = 'foo'

/** foo.js */
exports.name = 'foo'

/** main.js */
const list = ['./bar.js', './foo.js']
list.forEach(url => require(url)) // 动态导入
require(list[0]) // 动态导入

2.6 导入值的变化

CommonJs 导出的是值的拷贝,内部对值进行修改不会同步到外部。也可以对拷贝值进行修改。

/** bar.js */
var num = 0
var add = () => ++num
module.exports = { num, add }

/** main.js */
var { num, add } = require('./bar')
console.log(num) // 0
add()
console.log(num) // 0
num = 10
console.log(num) // 10

2.7 CommonJS 总结

  • CommonJS 会对加载结果进行缓存,不会多次执行;
  • CommonJS 是运行时加载模块;
  • CommonJS 导出的是值的拷贝,并可以对值进行修改;
  • CommonJS 的 require() 是同步加载模块。

三、ESModule

从ES6开始,js原生支持模块化,也就是推出了 ESModule,平时我们也会简称为 ESM。

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

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。

ESM 语法详细介绍——阮一峰 ES6 入门教程

3.1 export 导出

export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的 import 命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

// 导出变量
export const name = 'bar'
export const p = {
  name: '张三',
  age: 14,
}

// 导出函数
export function fn() {}
export const test = () => {}

// 批量多个导出
const cat = 'Tom'
const mouse = 'Jerry'
export {
  cat,
  mouse,
  cat as tom, // 使用as关键字重命名
  mouse as jerry,
}

// 默认导出,默认导出在同一个模块中只能有一个
export default {
  cat,
	mouse,
}

3.2 import 导入

/** module.js */
export var name = '张三'
export var age = 18

export default function () {
  console.log('module.js')
}

/** main.js */
// 批量导入
import { name, age } from './module'
console.log(name, age) // 张三 18

// 默认导入
import fn from './module'
console.log(fn) // [Function: default]

// 混合导入
import fun, { name as Name, age as Age } from './module'
console.log(fun, Name, Age) // [Function: default] 张三 18

// 模块的整体加载
import * as all from './module'
console.log(all) // [Module: null prototype] { age: 18, default: [Function: default], name: '张三' }

3.3 重复导入

/** module.js */
console.log('module.js 执行了~')
export {
	a: 10,
  b: 20
}

/** main.js */
import './module' // module.js 执行了~
import './module' // 不会执行,没有输出内容

import { a } from './module'
import { b } from './module'
// 以上两行代码等同于 import { a, b } from './module'
console.log(a, b) // 10 20

3.4 动态导入

因为 exportimport 需要处于模块顶部,并且是编译时加载,所以 import命令 无法实现动态导入,但在 ES2020提案 中引入了 import() 函数,支持了动态加载模块。

import() 返回一个 Promise 对象。

if (true) import xxx from 'XXX' // 报错

/** module.js */
export const name = 'module'

/** main.js */
const moduleName = './module.js'
if (true) {
  import(moduleName).then(({ name }) => {
    console.log(name) // module
  })
}

3.5 导入值的变化

export 导出的是值的引用,并且是只读的,内部对值进行修改会同步到外部。不可以对引用值进行修改。

/** module.js */
var num = 0
var add = () => ++num
export { num, add }

/** main.js */
import { num, add } from './module'
console.log(num) // 0
add()
console.log(num) // 1
num = 30 // 报错

3.6 自动使用严格模式

由于 ESModule 模块自动使用严格模式,其 顶层this 执行 undefined。

/** module.js */
console.log(this)

/** main.js */
import './module' // undefined

3.7 ESModule 总结

  • ESModule 会对加载结果进行缓存,不会多次执行;
  • ESModule 是编译时加载模块;
  • ESModule 导出的是值的引用,并且只读;
  • ESModule 会自动使用严格模式。

四、CJS和ESM的区别

  • CJS 模块是通过 module.exports 和 exports 导出,require() 导入,ESM 模块是通过 export 导出,import 导入;
  • CJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用;
  • CJS 模块是运行时加载,ESM 模块是编译时输出接口;
  • CJS 模块的 require() 是同步加载模块,ESM 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段;
  • ESM 模块会自动使用严格模式,不管你有没有在模块头部添加 "use strict"

你可能感兴趣的:(js笔记,javascript,前端)