Common.js规范和ES6模块

1,127 阅读6分钟

为什么需要CommonJs规范:

JS没有模块系统、标准库较少、缺乏包管理工具;为了让JS可以在任何地方运行,以达到Java、C#、PHP这些后台语言具备开发大型应用的能力;

定义和加载模块

在CommonJs规范中:

  1. 一个文件就是一个模块,拥有单独的作用域;

  2. 普通方式定义的变量、函数、对象都属于该模块内;

  3. 通过require来加载模块;

  4. 通过exportsmodule.exports来暴露模块中的内容;

模块中的所有代码都运行在模块作用域,不会污染全局作用域;同一个模块可以多次加载,但只会在第一次加载的时候运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果;模块的加载顺序,按照代码的出现顺序是同步加载的;

__dirname代表当前模块文件所在的文件夹路径,__filename代表当前模块文件所在的文件夹路径+文件名;

require(同步加载)基本功能:读取并执行一个JS文件,然后返回该模块的exports对象,如果没有发现指定模块会报错;

模块内的exports:为了方便,node为每个模块提供一个exports变量,其指向module.exports,相当于在模块头部加了这句话: var exports = module.exports,在对外输出时,可以给exports对象添加方法,PS:不能直接赋值(因为这样就切断了exportsmodule.exports的联系);

划重点

  1. 一个文件就是一个模块,模块中所有的代码都运行在模块作用域中。在模块中定义的变量、函数、对象都属于模块,不会污染全局作用域。
  2. 同一个模块可以加载多次,但只会在第一次加载的时候运行一次,然后运行结果就被缓存了,以后再加载就直接读取缓存结果。模块的加载方式是同步顺序加载。

1. 加载方式

CommonJS模块:运行时加载(动态加载)

举个栗子:

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

CommonJS模块就是对象,输入时必须查找对象属性。 上面代码的实质就是整体加载fs模块(加载fs的所有方法),生成一个_fs对象,然后再从这个对象上读取这三个方法。

ES6模块:编译时加载(静态加载)

同样举个栗子:

// ES6模块
import { stat, exists, readFile } from 'fs';

这行代码的实质是从fs模块加载3个方法,其他的方法不加载。这种加载称为编译时加载,即ES6可以在编译时就完成模块加载,效率比CommonJS模块的加载方式高。但是这也导致没法引用ES6模块本身,因为ES6模块不是对象

2. export命令

ES6模块的输出写法:

  1. export var/let + 标识符
  2. export function f(){}
  3. export class类
  4. 声明变量1,变量2;export {变量1,变量2,...}

export命令可以出现在模块的任何地方,只要处于模块的顶层就行,如果处于代码块或者函数体内就会报错。

3. import命令

import命令加载模块的方式:

  1. import {变量1,变量2,...} from "模块名"
// main.js
import {firstName, lastName, year} from './profile.js';

示例中import命令加载"profile.js",并从中输入变量,import命令接受一对大括号,里面指定要从其他模块导入的变量名,大括号里面的变量名必须与被导入模块的对外接口名称一致。如果想要为输入的变量重新去一个名字可以用as重命名变量。

import { lastName as surname } from './profile.js';

import具有提升效果,会提升至整个模块的头部,首先执行。import必须要处于模块顶层,不能使用表达式或者变量。

4. 模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有的输出值都加载在这个对象上面。

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
// main.js

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

注意:模块整体加载所在的那个对象是可以静态分析的,所以不可以运行时改变,所以下面这些写法是不允许的。

import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};

这里先提前说一下,模块的默认输出也包含在整体输出的这个对象之中,默认输出的变量名为default,也就是说可以通过整体输入的对象的的default属性获取模块的默认输出。默认输出(export default)会在下面讲到。

6. export default (模块默认输出)

export default指定模块的默认输出。 export default的使用方法:

  1. export default function(){} 或者function f(){} export default 后面可以接匿名函数或者具名函数。但是export后面接函数的时候必须是具名函数。
  2. export default 后面不能直接加变量声明
// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;

因为export default命令的实质是export 一个叫做default的变量,所以他后面不能加变量声明语句。 同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

// 正确
export default 42;

// 报错
export 42;

上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default

import()实现动态加载模块

上面说了import买了会被JavaScript引擎静态分析,会先于模块内其他语句执行,而且import和export命令只能在模块的顶层使用,不能出现在代码块中(比如在条件语句中或者函数中)。 这也导致import无法在运行时动态的加载模块。没法实现条件加载,这样的话import就无法取代Node的require方法。

const path = './' + fileName;
const module = require(path);

上面的代码就是动态加载,require要加载哪个模块只有运行时才知道。import命令做不到这一点。

ES2020提案引入import()函数,支持动态加载模块

import(path);

path为模块所在路径,import命令能接受什么参数,import()就能接受什么参数。两者区别就是import()为动态加载。

import()返回一个Promise对象

const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

import()类似于Node的require方法,区别主要是前者是异步加载(import()),后者是同步加载(require)。