esm是什么?

esm 是将 javascript 程序拆分成多个独自模块,并能按需导入的规范。和webpack,babel不同的是,esm 是 javascript 的规范性能,在浏览器端和 nodejs 中都已失去实现。应用 esm 的益处是浏览器能够最优化加载模块,比应用库更有效率。

esm 规范通过import, export语法实现模块变量的导入和导出。

esm 模块的特点

  • 存在模块作用域,顶层变量都定义在该作用域,内部不可见;
  • 模块脚本主动采纳严格模式;
  • 模块顶层的this关键字返回undefined;
  • esm 是编译时加载,也就是只有所有import的模块都加载实现,才会开始执行;
  • 同一个模块如果加载屡次,只会执行一次。

export
export语句用来导出模块中的变量。

// 导出变量export let count = 1;export const CONST_VAR = 'CONST_VAR';// 导出函数export function incCount() {    count += 1;}// 导出类export class Demo {}function add(x) {    return x + count;}// 应用export导出一组变量export {    count,    add,    // 应用as重命名导出的变量    add as addCount,}// 导出defaultexport default add// 合并导出其余模块的变量export { name } from './esm_module2.js'export * from './esm_module2.js'

import
import语句用来导入其余模块的变量

// 导入变量import { count, incCount, CONST_VAR } from './esm_module1.js';// 通过as重命名导入的变量import { addCount as renamedAddCount } from './esm_module1.js';// 导入默认import { default as defaultAdd } from './esm_module1.js';import add from './esm_module1.js';// 创立模块对象import * as module1 from './esm_module1.js';

export 导出的是值援用
esm 模块和 commonjs 模块的一个显著差别是,cjs 导出的是值得拷贝,esm 导出的是值的援用。当模块外部的值被批改时,cjs 获取不到被批改后的值,esm 能够获取到被批改后的值。

cjs 例子

// cjs_module1.jsvar count = 1;function incCount() {    count += 1;}module.exports = {    count: count,    incCount: incCount,}// cjs_demo.jsvar { count, incCount } = require('./cjs_module1.js');console.log(count); // 1incCount();console.log(count); // 1

esm 例子

// esm_module1.jslet count = 1;function incCount() {    count += 1;}export {    count,    incCount,}// esm_demo.jsimport { count, incCount } from './esm_module1.js';console.log(count); // 1incCount();console.log(count); // 2

从实现原理上来看,cjs 的 module.exports是一个对象,在运行期注入模块。在导出语句module.exports.count = count执行时,是给这个对象调配一个count的键,并赋值为1。 这之后模块中的count变量再怎么变动,都不会烦扰到module.exports.count

esm 中的export { count }是导出了count变量的一个只读援用,等于说使用者读取count时,值的指向还是模块中count变量的值。

能够看阮一峰的这篇文章:ES6入门教程

在 html 中应用 esm
应用script标签引入 esm 文件,同时设置type=module,标识这个模块为顶级模块。浏览器将 esm 文件视为模块文件,辨认模块的import语句并加载。

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

如果不设置type=module,浏览器认为该文件为一般脚本。查看到文件中存在import语句时,会报如下谬误:

esm的加载机制
esm 规范没有规定模块的加载细节,将这些留给具体环境实现。大抵上分为上面四个步骤:

解析:实现读取模块的源代码并查看语法错误;

加载:递归加载所有import的模块;

链接:对每个加载的模块,都生成一个模块作用域,该模块下的所有全局申明都绑定到该作用域上,包含从其余模块导入的内容;

运行时:实现所有import的加载和链接,脚本运行每个曾经加载的模块中的语句。当运行到全局申明时,什么也不会做(在链接阶段曾经将申明绑定到模块作用域)。

能够看下 mdn 上的这篇深刻 esm 的文章:ES6 In Depth: Modules

动静加载模块
esm 的一个重要个性是编译时加载,这有利于引擎的动态剖析。加载的过程会先于代码的执行。却也导致import导入语句不能在函数或者if语句中执行:

// 报语法错误if (true) {    import add from './esm_module1.js';}

es2020 提案引入import()函数,用来动静加载模块,并且能够用在函数和if语句中。

import('./esm_module1.js')  .then(module => {    console.log(module);  })

import()函数承受要加载的模块相对路径,返回一个Promise对象,内容是要加载的模块对象。

应用import()函数还能够实现依据变量动静加载模块

async function getTemplate(templateName) {    let template = await import(`./templates/${templateName}`);    console.log(template);}getTemplate("foo");getTemplate("bar");getTemplate("baz");