说说 CommonJS 中的 require 和 ES6 中的 import 区别?

提问

CommonJS 中的 require/exports 和 ES6 中的 import/export 区别?

回答

  • CommonJS 模块是运行时加载,ES6 Modules 是编译时加载并输出接口。
  • CommonJS 输出是值的拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变。
  • CommonJs 导入的模块路径可以是一个表达式,因为它使用的是 require() 方法,甚至这个表达式计算出来的内容是错误的路径,也可以通过编译到执行阶段再出错;而ES6 Modules 只能是字符串,并且路径不正确,编译阶段就会抛错。
  • CommonJS this 指向当前模块,ES6 Modules this 指向 undefined
  • ES6 Modules 中没有这些顶层变量:arguments、require、module、exports、__filename、__dirname

此总结出自 如何回答好这个高频面试题:CommonJS和ES6模块的区别?,笔者在这里做一些其他的分析

关于第一个差异运行时加载和编译时加载

这是最大的一个差别。commonjs 模块在引入时就已经运行了,它是“运行时”加载的;但 es6 模块在引入时并不会立即执行,内核只是对其进行了引用,只有在真正用到时才会被执行,这就是“编译时”加载(引擎在编译代码时建立引用)。很多人的误区就是 JS 为解释型语言,没有编译阶段,其实并非如此。举例来说 Chrome 的 v8 引擎就会先将 JS 编译成中间码,然后再虚拟机上运行。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

由此引发一些区别,如 require 理论上可以运用在代码的任何地方,可以在引入的路径里加表达式,甚至可以在条件判断语句里处理是否引入的逻辑。因为它是运行时的,在脚本执行时才能得知路径与引入要求,故而甚至时路径填写了一个压根不存在的地址,它也不会有编译问题,而在执行时才抛出错误。

// ...a lot code
if (true) {
  require(process.cwd() + '/a');    
}

但是 import 则不同,它是编译时的,在编译时就已经确定好了彼此输出的接口,可以做一些优化,而 require 不行。所以它必须放在文件开头,而且使用格式也是确定的,路径里不许有表达式,路径必须真实能找到对应文件,否则编译阶段就会抛出错误。

import a from './a'

// ...a lot code

关于第一个差异,是因为CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

关于第二点 CommonJS 输出的是值的拷贝 的补充

// a.js

var name = '张三';
var sex = 'male';
var tag = ['good look']

setTimeout(function () {
  console.log('in a.js after 500ms change ', name)
  sex = 'female';
  tag.push('young');
}, 500)

// exports.name = name;
// exports.sex = sex;
// exports.tag = tag;

module.exports = {
  name,
  sex,
  tag
}

// b.js
var a = require('./a');
setTimeout(function () {
  console.log(`after 1000ms in commonjs ${a.name}`, a.sex)
  console.log(`after 1000ms in commonjs ${a.name}`,  a.tag)
}, 1000)
console.log('in b.js');

若运行 b.js,得到下面的输出

$ node b.js
in b.js
in a.js after 500ms change  张三
after 1000ms in commonjs 张三 male
after 1000ms in commonjs 张三 [ 'good look', 'young' ]

把 a 和 b 看成两个不相干的函数,a 之中的 sex 是基础属性当然影响不到 b,而 a 和 b 的 tag 是引用类型,并且是共用一份地址的,自然 push 能影响。

补充说明 require 原理

require 是怎么做的?先根据 require('x') 找到对应文件,在 readFileSync 读取, 随后注入exports、require、module三个全局变量再执行源码,最终将模块的 exports 变量值输出

Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};

读取完毕后编译

Module.prototype._compile = function(content, filename) {
  var self = this;
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

上面代码等同于

(function (exports, require, module, __filename, __dirname) {
  // 模块源码
});

模块的加载实质上就是,注入exports、require、module三个全局变量,然后执行模块的源码,然后将模块的 exports 变量的值输出。

补充说明 Babel 下的 ES6 模块转化

Babel 也会将 export/import的时候,Babel也会把它转换为exports/require的形式。

// m1.js
export const count = 0;

// index.js
import {count} from './m1.js'
console.log(count)

Babel 编译后就应该是

// m1.js
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.count = void 0;
const count = 0;


// index.js
"use strict";

var _m = require("./m1.js");

console.log(_m.count);
exports.count = count;

正因为有 Babel 做了转化,所以 require 和 import 才能被混用在一个项目里,但是你应该知道这是两个不同的模块系统。

题外话

留个思考题给大家,这两种模块系统对于循环引用的区别?有关于循环引用是啥,参见我这篇Node 模块循环引用问题

posted @ 2020-05-15 23:05  Ever-Lose  阅读(829)  评论(0编辑  收藏  举报