前端面试题 - NodeJS能用ES6模块吗?CommonJS 和 ES6模块的区别是什么?
JS能写前端web,也能写NodeJS。
- Node.js 后端应用由模块组成,其模块系统采用 CommonJS 规范,它并不是 JavaScript 语言规范的正式组成部分。
- 前端的模块系统则采用ES6模块规范,这是 JavaScript 语言规范的正式组成部分。
但是现在技术进步了,后端也能用ES6模块规范(NodeJS支持),前端也能用Common JS规范(Webpack支持)。
- Node.js 默认用CommonJS规范,但也可以用ES6模块规范,但要求 ES6 模块采用.mjs后缀文件名。
也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。
Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。
如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
一旦设置了以后,该项目里面的.js文件,就被解释用 ES6 模块。
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。 - 在使用webpack等打包工具的前端项目中,默认用ES6规范,但也可以使用CommonJS,通过在项目的package.json文件中,指定type字段为commonjs,具体细节与NodeJS后端略有差异。
无论在前端还是在后端,import/export和require/module.exports也是可以在一个项目中同时使用,甚至相互混用,这样做需要一些配置,但是建议尽量不要混用。
一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易:
- 如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载。
- 如果原始模块是 CommonJS 格式,那么可以加一个ES6模块包装层(import该CommonJS 模块,然后再export出去)。
// CommonJS模块的ES6模块包装层
import cjsModule from '../index.js'; // index.js是CommonJS规范的
export const foo = cjsModule.foo;
ES6模块和Commonjs模块的相同点就是,二者对于同一模块多次加载都只会执行一次模块内代码,即首次加载执行,后面加载模块不执行其内部代码。
ES6模块 和 CommonJS的区别在于用法,加载时机和方式不同:
- CommonJS 模块使用require()加载和module.exports输出,require()是代码运行阶段同步加载JS文件的(运行时加载),后面的代码必须等待这个命令执行完(只加载JS文件,不执行JS文件内容)才会执行。
- ES6 模块使用import加载和export输出,为了不影响dom渲染异步加载JS文件,在JS代码静态解析阶段分析依赖关系,在代码运行前分析出export和import对应符号引用(同样只加载JS文件,不执行JS文件内容)。
JS代码在被JS引擎加载后,分为两个阶段:1、静态分析阶段(我们常说的编译阶段)2、运行阶段。静态分析阶段的主要工作是解析源码生成字节码。
ES6模块就是在静态分析阶段实现export和import的分析解析。
静态分析 & 静态作用域:如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。
它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量,这是固定的,这是非常有利于编译器做优化的。
因此export命令后面只能跟着声明式语句,而不能跟表达式(如变量名,字面量)。因为变量只有在声明时,才会产生一个变量引用的符号。
另外,ES6模块的export {} 中 {} 不是一个对象简写形式,更不是一个对象,而是export {} 语法组成部分,用作收集符号用。
另外,CommonJS的module.exports不是模块内部的变量,而是外部传入模块的变量,所以一旦模块内部代码对于exports变量做了修改,其实就是对于外部该变量做了修改,
因此模块代码未执行完,模块的输出module.exports也是有值的,因为这是外部值。
- ES6模块是在静态解析阶段分析import/export的输入输出的常/变量或函数,将其解析为一个“符号引用”(既不是一个对象,也不是一个变量,只是一种静态定义,一个简单的引用符号),
外部可以通过符号引用直接获取到对应模块中输出变量的实时数据。由于ES6输入的模块变量只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。 - CommonJS则在运行阶段加载模块输出对象,由于只作用于运行时导致完全没办法在编译时做“静态优化”,挂载在该对象上数据都是拷贝数据(浅拷贝),和原模块中的输出变量没有关系了。
外部获取module.exports,其实获取的是缓存数据(值都是初始化后的初始值),而不是原模块内的实时数据。如果访问原模块内的实时数据,通过函数返回内部值仍然是可以的。
建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
但人们往往说ES6模块是异步的,为什么呢?因为Common JS肯定是同步的,由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。
但ES6模块最初用于web,而传统的浏览器引入JS文件的方式就是使用script标签导入。
但是为了让script标签能够区分 模块JS文件 和 非模块JS文件,所以给script标签加入了 type = "module" 属性,来告诉浏览器引入的是ES6模块JS文件。
然后其他js文件内部就可以使用import xxx来引入该ES6模块JS文件里的内容了,就这样实现了模块化。
而设置了 type="module"的script标签,相当于带了 defer属性,即异步加载JS文件,不阻塞DOM构建,且会等DOM构建完成后,才执行JS模块代码(script标签默认是同步加载的)。
通俗易懂的前端面试题网站: https://www.front-interview.com
posted on
浙公网安备 33010602011771号