commonjs和ES6 Module的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

由于 CommonJS 并不是 ECMAScript 标准的一部分,所以 类似 module 和 require 并不是 JS 的关键字,module 是一个对象, require 是一个函数
而与此相对应的 ESM 中的 import 和 export 则是关键字,是 ECMAScript 标准的一部分

打印 module、require 查看细节:

console.log(module);
console.log(require);

// out:
Module {
  id: '.',
  path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  exports: {},
  filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules',
    '/Users/xxx/Desktop/esm_commonjs/node_modules',
    '/Users/xxx/Desktop/node_modules',
    '/Users/xxx/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}

[Function: require] {
  resolve: [Function: resolve] { paths: [Function: paths] },
  main: Module {
    id: '.',
    path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
    exports: {},
    filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
    loaded: false,
    children: [],
    paths: [
      '/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules',
      '/Users/xxx/Desktop/esm_commonjs/node_modules',
      '/Users/xxx/Desktop/node_modules',
      '/Users/xxx/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ]
  },
  extensions: [Object: null prototype] {
    '.js': [Function (anonymous)],
    '.json': [Function (anonymous)],
    '.node': [Function (anonymous)]
  },
  cache: [Object: null prototype] {
    '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js': Module {
      id: '.',
      path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
      exports: {},
      filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
      loaded: false,
      children: [],
      paths: [Array]
    }
  }
}
  • module 中的一些属性
    exports:这就是 module.exports 对应的值,由于还没有赋任何值给它,它目前是一个空对象。
    loaded:表示当前的模块是否加载完成。
    paths:node 模块的加载路径,这块不展开讲,感兴趣可以看node 文档
  • require
    main 指向当前当前引用自己的模块,所以类似 python 的 name == ‘main‘, node 也可以用 require.main === module 来确定是否是以当前模块来启动程序的。
    extensions 表示目前 node 支持的几种加载模块的方式。
    cache 表示 node 中模块加载的缓存,也就是说,当一个模块加载一次后,之后 require 不会再加载一次,而是从缓存中读取。

疑问:为什么 CommonJS 相互引用没有产生类似“死锁”的问题?

我们可以发现 CommonJS 模块相互引用时,没有产生类似死锁的问题。关键在 Module._load 函数里,具体源代码在这里。Module._load 函数主要做了下面这些事情:

  1. 检查缓存,如果缓存存在且已经加载,直接返回缓存,不做下面的处理
  2. 如果缓存不存在,新建一个 Module 实例
  3. 将这个 Module 实例放到缓存中
  4. 通过这个 Module 实例来加载文件
  5. 返回这个 Module 实例的 exports
    解释:
    当 app.js 加载 a.js 时,Module 会检查缓存中有没有 a.js,发现没有,于是 new 一个 a.js 模块,并将这个模块放到缓存中,再去加载 a.js 文件本身。在加载 a.js 文件时,Module 发现第一行是加载 b.js,它会检查缓存中有没有 b.js,发现没有,于是 new 一个 b.js 模块,并将这个模块放到缓存中,再去加载 b.js 文件本身。在加载 b.js 文件时,Module 发现第一行是加载 a.js,它会检查缓存中有没有 a.js,发现存在,于是 require 函数返回了缓存中的 a.js。但是其实这个时候 a.js 根本还没有执行完,还没走到 module.exports 那一步,所以 b.js 中 require(‘./a.js’) 返回的只是一个默认的空对象。所以最终会报 setA is not a function 的异常。

JavaScript 的执行过程

接下来我们要讲解 ESM 的模块导入,为了方便理解 ESM 的模块导入,这里需要补充一个知识点 —— JavaScript 的执行过程。

JavaScript 执行过程分为两个阶段:

  • 编译阶段
  • 执行阶段

编译阶段

在编译阶段 JS 引擎主要做了三件事:

  • 词法分析
  • 语法分析
  • 字节码生成

执行阶段

在执行阶段,会分情况创建各种类型的执行上下文,例如:全局执行上下文 (只有一个)、函数执行上下文。而执行上下文的创建分为两个阶段:

  • 创建阶段
  • 执行阶段

在创建阶段会做如下事情:

  1. 绑定 this
  2. 为函数和变量分配内存空间
  3. 初始化相关变量为 undefined

什么叫 编译时输出接口? 什么叫 运行时加载?

ESM 之所以被称为 编译时输出接口,是因为它的模块解析是发生在 编译阶段。

写到这里我们可以详细谈谈 ESM 的加载细节了,它其实和前面提到的 CommonJS 的 Module._load 函数做的事情有些类似:

  1. 检查缓存,如果缓存存在且已经加载,则直接从缓存模块中提取相应的值,不做下面的处理
  2. 如果缓存不存在,新建一个 Module实例
  3. 将这个 Module 实例放到缓存中
  4. 通过这个 Module 实例来加载文件加载文件后到全局执行上下文时,会有创建阶段和执行阶段,在创建阶段做函数和变量提升,接着执行代码
  5. 返回这个 Module 实例的 exports

ESM 和 CommonJS 的区别

不同点:this 的指向不同

  • commonjs this 指向的是当前 module 的默认 exports;
  • 而 ESM 由于语言层面的设计指向的是 undefined

不同点:filename,dirname 在 CommonJS 中存在,在 ESM 中不存在

  • CommonJS全局才可以直接用 __filename、__dirname。而 ESM 没有这方面的设计,所以在 ESM 中不能直接使用 __filename 和 __dirname

相同点:ESM 和 CommonJS 都有缓存


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 chaoyumail@126.com

×

喜欢就点赞,疼爱就打赏