请说说CommonJS和ES module的区别

CommonJS

1.module代表当前模块

CommonJS中,一个文件就是一个模块,模块中的变量、函数、类都是私有的外部不可以访问,并规定module代表当前模块,exports是对外的接口。

CommonJS主要依赖于module这个类,我们可以看一下module上面的相关属性:

Module {
    
    
  id: '.', // 如果是 mainModule id 固定为 '.',如果不是则为模块绝对路径
  exports: {
    
    }, // 模块最终 exports
  filename: '/absolute/path/to/entry.js', // 当前模块的绝对路径
  loaded: false, // 模块是否已加载完毕
  children: [], // 被该模块引用的模块
  parent: '', // 第一个引用该模块的模块
  paths: [ // 模块的搜索路径
   '/absolute/path/to/node_modules',
   '/absolute/path/node_modules',
   '/absolute/node_modules',
   '/node_modules'
  ]
}

2.为什么可以直接使用exports, module, _dirname这些方法属性

要回答这个问题我们要从CommonJS内部执行代码的原理说起。

CommonJS规范中代码在运行时会被包裹在一个立即执行函数中,之后我们会改变这个立即执行函数内部this的指向,指向的便是module.exports这个空对象。这便可以很好的解释我们node.js中内部this指向的是一个空对象的问题。

逻辑代码:

(function (exports, require, module, __filename, __dirname) {
    
    
    let name = "lm";
    exports.name = name;
});
jsScript.call(module.exports, args);

之后我们会给其传递exports, require, module, __filename等参数,所以我们可以在直接编写node.js代码中使用这些变量。

exports与module.exports有什么区别

node.js中我们导出一个变量、函数,或者类一般有两种到处方法。

function A() {
    
    
    console.log('过年好!');
}

// 法一:module.exports.A = A;
// 法二:exports.A = A;

这两种方法有什么区别呢?其实exports只是module.exports的引用罢了,所以实际上这两种方法在使用上的效果是一样的。

const module = {
    
    
    'exports': {
    
    
    }
}

const exports = module.exports;
exports.name = 'Andy'; //完全等价于 module.exports.name = 'Andy';

所以当我们使用exports或者module.exports导出模块时,其实也就是给module.exports这个对象添加属性,之后我们使用require引入模块时得到的便是module.exports这个对象。

注意:既然是对象属性的引用,所以当我们使用一个模块中的方法修改该模块中的变量,之后导出的变量的结果是不变的。也就是说只要一个变量已经被导出了,之后在模块内部对变量的修改都将无意义,这个情况要格外注意。(这点和ES module有很大的不同)

a.js

let count = 1;

function add() {
    
    
    count += 1;
}

exports.count = count;
exports.add = add;

b.js

let Module = require('./a');

console.log(Module.count); // 1
Module.add();
console.log(Module.count); // 1

4.模块引入后自动缓存

我们在使用require时可能是这样的:

let Module = require('./a');

如果是系统模块,或者第三方模块我们可以直接写模块名:

let fs = require('fs');

但实际上在require模块时我们都要根据计算机中的绝对地址来引入,这个根据相对地址或者包名来查找文件的过程是比较消耗时间的,我们可以通过 module.paths 来打印一下查找的过程:

[
  'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\node_modules',
  'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\node_modules',
  'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
  'c:\\Users\\dell\\Desktop\\node_modules',
  'c:\\Users\\dell\\node_modules',
  'c:\\Users\\node_modules',
  'c:\\node_modules'
]

所以为了提高效率,我们每次在文件中引入一个模块时,我们都会将引入的这个模块与其相应的绝对地址进行缓存,如果在一个文件中多次引入相同的模块这个模块只会被加载一次。

我们可以使用require.cache打印出当前模块的依赖模块看看,我们可以发现其是以绝对地址为key,模块为value的对象:

[Object: null prototype] {
    
    
  'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\b.js': Module {
    
    
    id: '.',
    path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动',
    exports: {
    
    },
    parent: null,
    filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\b.js',
    loaded: false,
    children: [ [Module] ],
    paths: [
      'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\node_modules',
      'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\node_modules',
      'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
      'c:\\Users\\dell\\Desktop\\node_modules',
      'c:\\Users\\dell\\node_modules',
      'c:\\Users\\node_modules',
      'c:\\node_modules'
    ]
  },
  'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\a.js': Module {
    
    
    id: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\a.js',
    path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动',
    exports: {
    
     count: 1, add: [Function: add] },
    parent: Module {
    
    
      id: '.',
      path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动',
      exports: {
    
    },
      parent: null,
      filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\b.js',
      loaded: false,
      children: [Array],
      paths: [Array]
    },
    filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\a.js',
    loaded: true,
    children: [],
    paths: [
      'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\字节跳动\\node_modules',
      'c:\\Users\\dell\\Desktop\\web-design\\前端相关题目\\node_modules',
      'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
      'c:\\Users\\dell\\Desktop\\node_modules',
      'c:\\Users\\dell\\node_modules',
      'c:\\Users\\node_modules',
      'c:\\node_modules'
    ]
  }
}

从而可以很好的解释这个例子:

// a.js
module.exports = {
    
    
    foo: 1,
};

// main.js
const a1 = require('./a.js');
a1.foo = 2;

const a2 = require('./a.js');

console.log(a2.foo); // 2
console.log(a1 === a2); // true

我们可以理解为只要模块一引入加载完,即使再次引用也还是之前的模块。

**同时缓存还很好的解决了循环引用的问题:**举个例子,现在有模块 a require模块b ;而模块b 又 require 了模块a。

// main.js
const a = require('./a');
console.log('in main, a.a1 = %j, a.a2 = %j', a.a1, a.a2);


// a.js
exports.a1 = true;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);


exports.a2 = true;
// b.js
const a = require('./a.js');
console.log('in b, a.a1 = %j, a.a2 = %j', a.a1, a.a2);

程序执行结果如下:

in b, a.a1 = true, a.a2 = undefined
in main, a.a1 = true, a.a2 = true

实际上在模块a代码执行之前就已经创建了Module实例写入了缓存,此时代码还没执行,exports是个空对象。

'/Users/evan/Desktop/module/a.js': 
   Module {
    
    
     exports: {
    
    },
     //...
  }
}

代码exports.a1 = true;修改了module.exports上的a1true,这时候a2代码还没执行。

'/Users/evan/Desktop/module/a.js': 
   Module {
    
    
     exports: {
    
    
      a1: true
    }
     //...
  }
}

进入b模块,require a.js时发现缓存上已经存在了,获取a模块上的exports。打印a1, a2分别是trueundefined

运行完b模块,继续执行a模块剩余的代码,exports.a2 = true;又往exports对象上增加了a2属性,此时module aexport对象,a1, a2均为true

exports: {
    
     
  a1: true,
  a2: true
}

再回到main模块,由于require('./a/js')得到的是module a export对象的引用,这时候打印a1, a2都为true

这里还有一个需要注意的点就是,模块在加载时是同步阻塞的,只有引入的模块加载完之后才执行后面的语句,大家记住就好。

5.总结

说了这么多我们主要的目的还是为了面试,所以这里小小的总结一下:

  • CommonJS中一个文件就是一个模块,模块中的变量、方法、类都是私有的
  • module代表当前模块,module.exports代表模块对外的接口
  • 模块在加载时所有内容会被放在一个立即执行函数中,函数的this指向是module.exports这个空对象,而exports只是module.exports的引用而已
  • 加载模块是同步阻塞的,加载后会进行缓存,多次引入只会加载一次
  • require得到的模块中变量、方法、类的拷贝,并不是直接的引用

ES module

这个是我们最常用的,我们通常会在Vue或者Webpack中来使用,其并像是CommonJS那样将代码放在一个立即执行函数中(依靠闭包)从而实现模块化,而是从语法层面完成的模块化。一般情况下我们写的ES module语法还是会通过babel或者Webpack等工具转化为CommonJS语法。

对于ES module就不详细介绍其实现原理了,主要想说一下其特点并且和CommonJS相比有什么区别来方便大家记忆。

1.在执行模块前会先加载所有的依赖模块

这点也是最重要的一点,通过上面我们知道CommonJS是在执行到需要加载依赖模块时,会(同步阻塞)停下当前任务去加载相应的依赖模块,而对于ES module来说无论你在哪一行引用依赖模块,其都会在一开始就进行加载相应的依赖模块。

// a.mjs
export const a1 = true;
import * as b from './b.mjs';
export const a2 = true;

// b.mjs
import {
    
     a1, a2 } from './a.mjs'
console.log(a1, a2);

在这种情况下,如果之前的CommonJS会输出trueundefined,而现在会直接报错:ReferenceError: Cannot access 'a1' before initialization。

同样的原因我们在CommonJS中可以这样写,而在ES module中会报错:

require(path.join('xxxx', 'xxx.js'))

在这里插入图片描述
同样如果我们在CommonJS中引入一个没有exports的变量那么在代码执行时才会报错,而在ES module在刚开始的时候就会报错。

2.import的是变量的引用

CommonJS的情况下:

// counter.js
let count = 1;

function increment () {
    
    
  count++;
}

module.exports = {
    
    
  count,
  increment
}

// main.js
const counter = require('counter.cjs');

counter.increment();
console.log(counter.count); // 1

ES module情况下:

// counter.mjs
export let count = 1;

export function increment () {
    
    
  count++;
}

// main.mjs
import {
    
     increment, count } from './counter.mjs'

increment();
console.log(count); // 2

这一次我们导入是变量的引用了,这样可以避免之前CommonJS在实际开发中的很多问题,实际类似于这样。

exports.counter = 1;

exports.increment = function () {
    
    
    exports.counter++;
}

3.ES module是部分导入

这个很好理解,在CommonJS中我们加载一个模块需要将该模块的所有接口导入进来,而ES module里我们可以按需只导入我们想要的接口。

最后顺便再提一点: 处于兼容性考虑对于像Webpack我们在使用的ES module时最终还是会转换为CommonJS规范,所有有些时候我们使用require时导入的并不是目标值,我们往往需要加一个.default才行,这是因为ES moduleexport default语法所造成的。

4.总结

其实ES module相对于CommonJS最大的区别就是两点:

  • 在执行模块前首先要加载所有的依赖模块,如果加载有问题直接报错
  • ES module的模块引入的变量、函数、类的引用这是很有先进性的

还是值得一提的就是ES module可以按需引入自己需要的接口,两者也是具有相同点的就是都会对已经引入的模块进行缓存,如果多次引入只会执行一次。

QQ:505417246
微信:18331092918
微信公众号:Code程序人生
个人博客:http://rayblog.ltd

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_46171043/article/details/120732407
今日推荐