JS笔记《Module》

106 阅读4分钟

概述

  • 历史上,JS一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。ES6新增的模块功能主要由两个命令构成:exportimportexport用于模块的对外接口,import用于输入其他模块提供的功能。

export

  • 一个模块就是一个独立的文件,该文件内部的所有变量外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export输出该变量。
  • export命令可以出现在模块的任何位置,但必须处于模块顶层,如果处于块级、函数作用域中时,就会报错。
  • 以下代码在一个profile.js文件中,被视为一个模块,使用export对外输出了三个变量。
// 写法一:一个一个导出
export let firstName = 'Michael';
export let lastName = 'Jackson';
export let year = 1958;


// 写法二:导出一组(推荐)
let firstName = 'Michael';
let lastName = 'Jackson';
let year = 1958;
export { firstName, lastName, year };
  • 除了输出变量,还可以输出函数或类。
// 写法一
export function multiply(x, y) {
  return x * y;
};


// 写法二(推荐)
function multiply(x, y) {
  return x * y;
};
export { multiply };
  • 通常情况下,export输出的就是本来的名字,但是可以使用as重命名。
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

export default

  • 为模块指定默认输出,一个模块只能由一个默认输出,所以export default只能使用一次。
  • import加载该模块时,可以为该匿名函数指定任意名字,且不用加大括号。
export default function foo() {
  console.log('foo');
}

// 或者写成
function foo() {
  console.log('foo');
}
export default foo;

// 加载
import customName from 'xxx.js'; // customName就是 foo函数
  • 本质export default就是输出一个叫做default的变量或函数,然后系统允许为它取任意名字。
function add(x, y) {
  return x * y;
}

export default add; 
// 等同于
export { add as default };


import custom from 'xxx.js';
// 等同于
import { default as custom } from 'xxx.js';
  • 由于输出的是default的变量,所以它后面不能跟变量声明语句。
let a = 1;

export { a };      // 正确

export default a;  // 正确

export default 1;  // 正确

export default let a = 1;  // 错误

import

  • 使用export定义了模块的对外接口以后,其他 JS 文件就可以通过import加载这个模块。大括号里面的变量名,必须与被导入模块对外接口的名称相同。
// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
  • 如果想为输入的变量重新取一个名字,import要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
  • import输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
import { a } from './xxx.js'

a = {};  // Syntax Error : 'a' is read-only;
  • import具有提升效果,会提升到整个模块的头部,首先执行。
foo();

import { foo } from 'my_module';

整体加载

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

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

// 逐一加载
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));


// 整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

动态加载

  • import()函数,支持动态加载模块,什么时候运行到这一句,什么时候才会加载指定的模块。参数为要加载的模块的位置。返回一个Promise对象。
// test.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;


// 只有用户点击了按钮,才会加载这个模块
button.addEventListener('click', event => {
  import('./test.js')
  .then({firstName, lastName} => {
    document.write(firstName + lastName);
  })
  .catch(error => {
    /* Error handling */
  })
});
  • 推荐使用await处理。
button.addEventListener('click', async event => {
  const {firstName, lastName} = await import('./test.js')
  document.write(firstName + lastName);
});
  • import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});
  • 如果模块有default输出接口,可以用参数直接获得。
import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

浏览器加载模块

  • 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。
<script type="module" src="./foo.js"></script>

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。如果有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

  • 内嵌在网页中的模块加载:
<script type="module">
  import utils from "./utils.js";
  
  // use utils do something 
</script>