深入理解ES6--13.用模块封装代码

1,422 阅读9分钟

原创文章&经验总结&从校招到A厂一路阳光一路沧桑

详情请戳www.codercc.com

主要知识点:什么是模块、模块中的导出、模块中的导入

1. 什么是模块?

模块(Modules ) 是使用不同方式加载的 JS 文件(与 JS 原先的脚本加载方式相对) 。这种不同模式很有必要,因为它与脚本(script ) 有大大不同的语义:

  1. 模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式;
  2. 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在;
  3. 模块顶级作用域的 this 值为 undefined
  4. 模块不允许在代码中使用 HTML 风格的注释(这是 JS 来自于早期浏览器的历史遗留特性) ;
  5. 对于需要让模块外部代码访问的内容,模块必须导出它们;
  6. 允许模块从其他模块导入绑定;

1.1 基本的导出

可以使用 export 关键字将已发布代码部分公开给其他模块。最简单方法就是将 export放置在任意变量、函数或类声明之前:

// 导出数据
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// 导出函数
export function sum(num1, num2) {
		return num1 + num1;
} 
// 导出类
export class Rectangle {
	constructor(length, width) {
	this.length = length;
	this.width = width;
	}
} 
// 此函数为模块私有
function subtract(num1, num2) {
		return num1 - num2;
} 
// 定义一个函数……
function multiply(num1, num2) {
		return num1 * num2;
} 
// ……导出一个函数引用
export { multiply };

此例中有几点需要注意。首先,除了 export 关键字之外,每个声明都与正常形式完全一样。每个被导出的函数或类都有名称,这是因为导出的函数声明与类声明必须要有名称。你不能使用这种语法来导出匿名函数或匿名类,除非使用了 default 关键字 。其次,细看一下 multiply() 函数,它并没有在定义时被导出。这是因为你不仅能导出声明,还可以导出引用(即代码最后一行) 。最后请注意,此例并未导出subtract() 函数。此函数在模块外部不可访问,因为任意没有被显式导出的变量、函数或类都会在模块内保持私有。

1.2 基本的导入

一旦你有了包含导出的模块,就能在其他模块内使用 import 关键字来访问已被导出的功能。 import 语句有两个部分,一是需要导入的标识符,二是需导入的标识符的来源模块。此处是导入语句的基本形式:

import { identifier1, identifier2 } from "./example.js";

import 之后的花括号指明了从给定模块导入对应的绑定, from 关键字则指明了需要导入的模块。模块由一个表示模块路径的字符串(被称为模块说明符, module specifier ) 来指定。

当从模块导入了一个绑定时,该绑定表现得就像使用了 const 的定义。这意味着你不能再定义另一个同名变量(包括导入另一个同名绑定) ,也不能在对应的 import 语句之前使用此标识符(也就是要受暂时性死区限制) ,更不能修改它的值。

导入单个绑定

实现导入单个绑定时,仅仅只需要使用一个标识符:

// 单个导入
import { sum } from "./example.js";
console.log(sum(1, 2)); // 3
sum = 1; // 出错

对于已导入的绑定再重新赋值,则会导致错误。

导入多个绑定

如果你想从 example 模块导入多个绑定,你可以像下面这样显式的列出它们:

// 多个导入
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1, 2)); // 2

此处从 example 模块导入了三个绑定: summultiplymagicNumber

完全导入一个模块

还有一种特殊情况,即允许你将整个模块当作单一对象进行导入,该模块的所有导出都会作为对象的属性存在。例如:

// 完全导入
import * as example from "./example.js";
console.log(example.sum(1,
example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2

在此代码中, example.js 中所有导出的绑定都被加载到一个名为 example 的对象中,具名导出( sum() 函数、 multiple() 函数与 magicNumber ) 都成为 example可用属性。这种导入格式被称为命名空间导入(namespace import ) ,这是因为该 example 对象并不存在于 example.js 文件中,而是作为一个命名空间对象被创建使用,其中包含了example.js 的所有导出成员。

需要注意的是:无论对同一个模块使用了多少次 import 语句,该模块都只会被执行一次。在导出模块的代码执行之后,已被实例化的模块就被保留在内存中,并随时都能被其他 import 所引用

导入绑定无法修改原始值

ES6 的 import 语句为变量、函数与类创建了只读绑定,而不像普通变量那样简单引用了原始绑定。尽管导入绑定的模块无法修改绑定的值,但是可以在导出模块中对原始值做出修改,导入绑定会自动反映出修改的变化,例如:

导出模块:

export var name = "Nicholas";
export function setName(newName) {
	name = newName;
}

导入模块:

import { name, setName } from "./example.js";
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
name = "Nicholas"; // error

调用 setName("Greg") 会回到导出 setName() 的模块内部,并在那里执行,从而将 name 设置为 "Greg" 。注意这个变化会自动反映到所导入的 name 绑定上。

1.3 重命名的导出与导入

在导出模块中进行重命名

如果想用不同的名称来导出,可以使用 as 关键字来定义新的名称:

function sum(num1, num2) {
	return num1 + num2;
} 
export { sum as add };

此处的 sum() 函数被作为 add() 导出,前者是本地名称(local name ) ,后者则是导出名称(exported name ) 。这意味着当另一个模块要导入此函数时,它必须改用 add 这个名称:

import {add} from './example.js'

在导入时重命名

在导入时同样可以使用 as 关键字进行重命名:

import { add as sum } from './example.js'
console.log(typeof add); // "undefined"
console.log(sum(1, 2)); // 3

此代码导入了add() 函数,并使用了导入名称(import name ) 将其重命名为 sum()(本地名称) 。这意味着在此模块中并不存在名为 add 的标识符。

2. 模块的默认值

模块的默认值( default value ) 是使用 default 关键字所指定的单个变量、函数或类,而你在每个模块中只能设置一个默认导出,将 default 关键字用于多个导出会是语法错误。

2.1 导出默认值

导出默认值一共有三种形式:

  1. 不使用标识符
export default function(num1,num2){
	return num1+num2;
}

此模块将一个函数作为默认值进行了导出, default 关键字标明了这是一个默认导出,此函数并不需要有名称。

  1. 使用标识符
function sum(num1, num2) {
	return num1 + num2;
} 
export default sum;
  1. 使用重命名语法
function sum(num1, num2) {
	return num1 + num2;
} 
export {sum as default};
  1. 既导出了默认值,又导出非默认值
export let color = 'red';
export default function(num1,num2){
	return num1+num2;
}

2.2 导入默认值

只导入默认值

import sum from './example.js';
console.log(sum(1,2));

这个导入语句从 example.js 模块导入了其默认值。与之前在非默认的导入中看到的不同,注意此处并未使用花括号。本地名称 sum 被用于代表目标模块所默认导出的函数。

既导入默认值,又导入非默认值

import sum, { color } from './example.js';
console.log(sum(1,2));
console.log(color);

逗号将默认的本地名称与非默认的名称分隔开,非默认值仍旧被花括号所包裹。要记住在 import 语句中默认名称必须位于非默认名称之前。

对导入默认值重命名

import {default as sum, color} from './example.js'

console.log(sum(1,2));
console.log(color);

在此代码中,默认的导出( default ) 被重命名为 sum ,并且附加的 color 导出也被一并导入了。

2.3 对已导入的内容再导出

如果在当前模块中对已导入的内容在导出:

export {sum} from './example.js';

这种形式的 export 会进入指定模块查看 sum 的定义,随后将其导出。在导出时同样也可以进行重命名:

export { sum as add } from './example.js'

如果想将另一个模块中的所有值完全导出,可以使用 * 号模式:

export * from './example.js';

用完全导出,就可以导出目标模块的默认值及其所有具名导出,但这可能影响你从当前模 块所能导出的值。例如,假设 example.js 具有一个默认导出,当你使用这种语法时,你就无法为当前模块另外再定义一个默认导出。

2.4 无绑定的导入

有些模块也许没有进行任何导出,相反只是修改全局作用域的对象。尽管这种模块的顶级变量、函数或类最终并不会自动被加入全局作用域,但这并不意味着该模块无法访问全局作用域。诸如 ArrayObject 之类的内置对象的共享定义在模块内部是可访问的,并且对于这些对象的修改会反映到其他模块中。

3. 总结

  1. ES6 为 JS 语言添加了模块,作为打包与封装功能的方式。模块的行为异于脚本,它们不会用自身顶级作用域的变量、函数或类去修改全局作用域,而模块的 this 值为 undefined
  2. 可以在模块中使用 export 关键字导出,变量、函数与类都可以,并且每个模块允许存在一个默认导出。在导出之后,另一个模块就能导入该模块所导出的一个或多个导出值。这些导入的名称就像是被 const 所定义的,会被当作块级绑定,并且不允在同一模块内重复声明;
  3. 由于模块必须用与脚本不同的方式运行,浏览器就引入了 <script type="module"> ,以表示资源文件或内联代码需要作为模块来执行。使用