这个问题也能够变为 commonjs模块和ES6模块的区别;下面就经过一些例子来讲明它们的区别。前端
先来一道面试题测验一下:下面代码输出什么node
// base.js let count = 0; setTimeout(() => { console.log("base.count", ++count); // 1 }, 500) module.exports.count = count; // commonjs.js const { count } = require('./base'); setTimeout(() => { console.log("count is" + count + 'in commonjs'); // 0 }, 1000) // base1.js let count = 0; setTimeout(() => { console.log("base.count", ++count); // 1 }, 500) exports const count = count; // es6.js import { count } from './base1'; setTimeout(() => { console.log("count is" + count + 'in es6'); // 1 }, 1000) 复制代码
注意上面的ES6模块的代码不能直接在 node 中执行。能够把文件名称后缀改成
.mjs
, 而后执行node --experimental-modules es6.mjs
,或者自行配置babel。es6
CommonJs 规范规定,每一个模块内部,module
变量表明当前模块。这个变量是一个对象,它的 exports
属性(即module.exports
)是对外的接口,加载某个模块,实际上是加载该模块的module.exports
属性。面试
const x = 5; const addX = function (value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; 复制代码
上面代码经过module.exports输出变量x和函数addX。数组
require方法用于加载模块。缓存
const example = require('./example.js'); console.log(example.x); // 5 console.log(example.addX(1)); // 6 复制代码
CommonJS 模块的特色以下:微信
Node内部提供一个Module构建函数。全部模块都是Module的实例。babel
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; // ... } 复制代码
每一个模块内部,都有一个module对象,表明当前模块。它有如下属性。markdown
module.exports属性表示当前模块对外输出的接口,其余文件加载该模块,实际上就是读取module.exports变量。app
为了方便,Node为每一个模块提供一个exports变量,指向module.exports。这等同在每一个模块头部,有一行这样的命令
const exports = module.exports; 复制代码
注意,不能直接将exports变量指向一个值,由于这样等于切断了exports与module.exports的联系。
exports = function(x) {console.log(x)}; 复制代码
上面这样的写法是无效的,由于exports再也不指向module.exports了。
下面的写法也是无效的。
exports.hello = function() { return 'hello'; }; module.exports = 'Hello world'; 复制代码
上面代码中,hello函数是没法对外输出的,由于module.exports被从新赋值了。
这意味着,若是一个模块的对外接口,就是一个单一的值,最好不要使用exports输出,最好使用module.exports输出。
module.exports = function (x){ console.log(x);}; 复制代码
若是你以为,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
第一次加载某个模块时,Node会缓存该模块。之后再加载该模块,就直接从缓存取出该模块的module.exports属性。
require('./example.js'); require('./example.js').message = "hello"; require('./example.js').message // "hello" 复制代码
上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。可是第三次加载的时候,这个message属性依然存在,这就证实require命令并无从新加载模块文件,而是输出了缓存。
若是想要屡次执行某个模块,可让该模块输出一个函数,而后每次require这个模块的时候,从新执行一下输出的函数。
全部缓存的模块保存在require.cache之中,若是想删除模块的缓存,能够像下面这样写。
// 删除指定模块的缓存 delete require.cache[moduleName]; // 删除全部模块的缓存 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; }) 复制代码
注意,缓存是根据绝对路径识别模块的,若是一样的模块名,可是保存在不一样的路径,require命令仍是会从新加载该模块。
ES6 模块的设计思想是尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。好比,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块 let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile; 复制代码
上面代码的实质是总体加载fs模块(即加载fs的全部方法),生成一个对象(_fs),而后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,由于只有运行时才能获得这个对象,致使彻底没办法在编译时作“静态优化”。
ES6 模块不是对象,而是经过export命令显式指定输出的代码,再经过import命令输入。
// ES6模块 import { stat, exists, readFile } from 'fs'; 复制代码
上面代码的实质是从fs模块加载 3 个方法,其余方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 能够在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。固然,这也致使了无法引用 ES6 模块自己,由于它不是对象。
ES6的模块功能主要由两个命令构成:export
和 import
。 export 命令用于规定模块的对外接口。import 命令用于输入 其余模块提供的功能。
export const firstName = 'Michael'; export function multiply(x, y) { return x * y; }; 复制代码
// 报错 export 1; // 报错 const m = 1; export m; 复制代码
上面两种写法都会报错,由于没有提供对外的接口。第一种写法直接输出 1,第二种写法经过变量m,仍是直接输出 1。1只是一个值,不是接口。
// 写法一 export const m = 1; // 写法二 const m = 1; export {m}; // 写法三 const n = 1; export {n as m}; 复制代码
import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only; 复制代码
上面代码中,脚本加载了变量a,对其从新赋值就会报错,由于a是一个只读的接口。可是,若是a是一个对象,改写a的属性是容许的。
import {a} from './xxx.js' a.foo = 'hello'; // 合法操做 复制代码
上面代码中,a的属性能够成功改写,而且其余模块也能够读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都看成彻底只读,不要轻易改变它的属性。
foo(); import { foo } from 'my_module'; 复制代码
这种行为的本质是,import命令是编译阶段执行的,在代码运行以前。
// 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module; 复制代码
import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module'; 复制代码
上面代码中,虽然foo和bar在两个语句中加载,可是它们对应的是同一个my_module实例。也就是说,import语句是 Singleton
模式。
export default
就是输出一个叫作default的变量或方法export default
因此它后面不能跟变量声明语句export default
就是输出一个叫作default的变量或方法,而后系统容许你为它取任意名字。// modules.js function sayHello() { console.log('哈哈哈') } export { sayHello as default}; // 等同于 // export default sayHello; // app.js import { default as sayHello } from 'modules'; // 等同于 // import sayHello from 'modules'; 复制代码
// 正确 export const a = 1; // 正确 const a = 1; export default a; // 错误 export default const a = 1; 复制代码
上面代码中,export default a的含义是将变量a的值赋给变量default。因此,最后一种写法会报错。
一样地,由于export default命令的本质是将后面的值,赋给default变量,因此能够直接将一个值写在export default以后。
// 正确 export default 42; // 报错 export 42; 复制代码
上面代码中,后一句报错是由于没有指定对外的接口,而前一句指定对外接口为default。
export { foo, bar } from 'my_module'; // 能够简单理解为 import { foo, bar } from 'my_module'; export { foo, bar }; 复制代码
写成一行之后,foo和bar实际上并无被导入当前模块,只是至关于对外转发了这两个接口,致使当前模块不能直接使用foo和bar。
export { es6 as default } from './someModule'; // 等同于 import { es6 } from './someModule'; export default es6; 复制代码
在日常开发中这种常被用到,有一个utils目录,目录下面每一个文件都是一个工具函数,这时候常常会建立一个index.js文件做为 utils的入口文件,index.js中引入utils目录下的其余文件,其实这个index.js其的做用就是一个对外转发 utils 目录下 全部工具函数的做用,这样其余在使用 utils 目录下文件的时候能够直接 经过 import { xxx } from './utils'
来引入。
第二个差别是由于 CommonJS 加载的是一个对象(即module.exports属性)。该对象只有在脚本运行完才会生成。而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态编译阶段就会生成。
在传统编译语言的流程中,程序中的一段源代码在执行以前会经历三个步骤,统称为编译。”分词/词法分析“ -> ”解析/语法分析“ -> "代码生成"。
下面来解释一下第一个区别 CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。
// lib.js const counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; 复制代码
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。而后,在main.js里面加载这个模块。
// main.js const mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3 复制代码
上面代码说明,lib.js 模块加载之后,它的内部变化就影响不到输出的 mod.counter了。这是由于 mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能获得内部变更后的值
// lib.js const counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; 复制代码
上面代码中,输出的counter属性其实是一个取值器函数。如今再执行main.js,就能够正确读取内部变量counter的变更了。
3 4 复制代码
ES6 模块的运行机制与 CommonJS 不同。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号链接”,原始值变了,import加载的值也会跟着变。所以,ES6 模块是动态引用,而且不会缓存值,模块里面的变量绑定其所在的模块。
仍是举上面的例子。
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4 复制代码
上面代码说明,ES6 模块输入的变量counter是活的,彻底反应其所在模块lib.js内部的变化。
再举一个出如今export一节中的例子。
// m1.js export const foo = 'bar'; setTimeout(() => foo = 'baz', 500); // m2.js import {foo} from './m1.js'; console.log(foo); setTimeout(() => console.log(foo), 500); 复制代码
上面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。
让咱们看看,m2.js可否正确读取这个变化。
bar
baz
复制代码
上面代码代表,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,而且变量老是绑定其所在的模块。
因为 ES6 输入的模块变量,只是一个“符号链接”,因此这个变量是只读的,对它进行从新赋值会报错。
// lib.js export let obj = {}; // main.js import { obj } from './lib'; obj.prop = 123; // OK obj = {}; // TypeError 复制代码
上面代码中,main.js
从lib.js输入变量obj,能够对obj添加属性,可是从新赋值就会报错。由于变量obj指向的地址是只读的,不能从新赋值,这就比如main.js创造了一个名为obj的const变量。
最后,export经过接口,输出的是同一个值。不一样的脚本加载这个接口,获得的都是一样的实例。
// mod.js function C() { this.sum = 0; this.add = function () { this.sum += 1; }; this.show = function () { console.log(this.sum); }; } export let c = new C(); 复制代码
上面的脚本mod.js,输出的是一个C的实例。不一样的脚本加载这个模块,获得的都是同一个实例。
// x.js import {c} from './mod'; c.add(); // y.js import {c} from './mod'; c.show(); // main.js import './x'; import './y'; 复制代码
如今执行main.js,输出的是1。
这就证实了x.js和y.js加载的都是C的同一个实例。
在日常开发中这种常被用到,有一个utils目录,目录下面每一个文件都是一个工具函数,这时候常常会建立一个index.js文件做为 utils的入口文件,index.js中引入utils目录下的其余文件,其实这个index.js其的做用就是一个对外转发 utils 目录下 全部工具函数的做用,这样其余在使用 utils 目录下文件的时候能够直接 经过 import { xxx } from './utils' 来引入。
下面代码输出什么
// index.js console.log('running index.js'); import { sum } from './sum.js'; console.log(sum(1, 2)); // sum.js console.log('running sum.js'); export const sum = (a, b) => a + b; 复制代码
答案: running sum.js, running index.js, 3
。
import命令是编译阶段执行的,在代码运行以前。所以这意味着被导入的模块会先运行,而导入模块的文件会后执行。 这是CommonJS中require()和import之间的区别。使用require(),您能够在运行代码时根据须要加载依赖项。 若是咱们使用require而不是import,running index.js,running sum.js,3会被依次打印。
// module.js export default () => "Hello world" export const name = "Lydia" // index.js import * as data from "./module" console.log(data) 复制代码
答案:{ default: function default(), name: "Lydia" }
使用import * as name语法,咱们将module.js文件中全部export导入到index.js文件中,而且建立了一个名为data的新对象。 在module.js文件中,有两个导出:默认导出和命名导出。 默认导出是一个返回字符串“Hello World”的函数,命名导出是一个名为name的变量,其值为字符串“Lydia”。 data对象具备默认导出的default属性,其余属性具备指定exports的名称及其对应的值。
// counter.js let counter = 10; export default counter; // index.js import myCounter from "./counter"; myCounter += 1; console.log(myCounter); 复制代码
答案:Error
引入的模块是 只读 的: 你不能修改引入的模块。只有导出他们的模块才能修改其值。 当咱们给myCounter增长一个值的时候会抛出一个异常: myCounter是只读的,不能被修改。
最近发起了一个100天前端进阶计划,主要是深挖每一个知识点背后的原理,欢迎关注 微信公众号「牧码的星星」,咱们一块儿学习,打卡100天。