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

提问

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

回答

  • 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模块的区别?,笔者在这里作一些其余的分析node

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

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

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

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

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

可是 import 则不一样,它是编译时的,在编译时就已经肯定好了彼此输出的接口,能够作一些优化,而 require 不行。因此它必须放在文件开头,并且使用格式也是肯定的,路径里不准有表达式,路径必须真实能找到对应文件,不然编译阶段就会抛出错误。app

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 能影响。ui

补充说明 require 原理

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

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 模块循环引用问题

相关文章
相关标签/搜索