乐趣区

关于es6:JavaScript模块化从闭包到ES-Module

模块化是一种将 JavaScript 程序拆分为可按需导入的独自模块的机制,随着现在 JavaScript 脚本体积越来越大、越来越简单,JavaScript 的模块化机制也变得越来越重要,当初,简直所有最新浏览器都反对 js 原生模块化机制。

模块化的意义何在?

js 模块化机制将 js 代码拆分到不同的细小文件中,有以下长处:

  • 每个文件都具备公有命名空间,防止全局净化、变量抵触。
  • 逻辑拆散,能够将不同逻辑代码放在不同的 js 文件中。
  • 进步代码的可复用性、可维护性和可读性

基于对象、闭包的模块化

基于对象的模块化

在 CommonJs、ES6Module 呈现以前,为了防止全局变量净化,罕用的一种办法就是将一类变量放到一个对象中,这样每个对象里的属性(变量)就都是该对象公有的,防止了变量抵触的问题。

let a = {sayHello: 'hello1'}
let b = {sayHello: 'hello2'}

这样即便呈现雷同的变量名,也不会造成抵触,将每个逻辑点相干的变量放到一个对象中,尽量减少变量净化的状况。js 内置对象 Math 也是由这种思路实现的。

基于闭包的模块化

IIFE(立刻调用函数表达式)

IIFE 是一个定义时就会调用的函数,定义一个 IIFE 很简略,只须要写两个小括号,第一个括号里申明一个匿名函数,第二个括号里传入实参。

// IIFE 有两种写法格调,两种都能够失常应用
(function (arg) {console.log(arg)
})(1); // IIFE 前面必须要加分号,示意完结
// 1

(function (arg) {console.log(arg)
}(2)); // IIFE 前面必须要加分号,示意完结
// 2

立刻调用函数表达式(IIFE)具备以下长处:

  • 函数内的变量不会造成全局净化。
  • 函数执行完后就会立刻销毁,不会造成资源节约。

设想一下有一个工具,它能解析代码文件,将每个文件的内容包装成一个立刻调用函数表达式中,还能够跟踪每个函数的返回值,将所有内容拼装成一个大文件。

一些代码打包工具就是基于这种思维实现的。

两种形式的不足之处

两种办法尽管都能实现公有命名空间,防止变量净化问题,然而依然存在一些显著的缺点。

对于基于对象实现的模块化而言:

  • 申明变量就变成了申明对象的一个属性,没方法应用申明变量的一些有用机制,可能会导致反复命名属性造成属性笼罩的问题。
  • 对象之间可能呈现笼罩的状况
  • 代码仍在一个文件里,会导致文件代码量越来越大
// 意外地笼罩了属性 b
let o = {b: 1}
o.a = 1;
o.b = 2; // 笼罩了属性 b,并且 js 不会呈现任何提醒

let a = 1;
let b = 2;

let b = 3; // js 会报错,提醒不能反复申明

对于基于闭包的模块化而言:

  • IIFE 中的变量和函数不可复用
  • 应用不不便
  • 难以测试,难以保护

此外两种形式都并不是真正的现实下的模块化,都存在不能将代码拆分到不同文件中,难以保护,公有命名空间的实现有缺点等问题。

Node.js 的模块化(CommonJs)

现实中的模块化,应该是能够将不同代码拆分到不同的文件中,这样有利于可维护性和可读性,不同代码文件之间能够相互导入,有利于可复用性,每个代码文件都具备公有命名空间,这样能够防止变量抵触和净化。

CommonJs 模块机制实现了以上要求,CommonJs 是 Nodejs 内置的模块化机制。它能够将简单的程序拆分成任意多个代码文件,每个文件都是一个领有公有命名空间的独立模块,能够抉择导出其中一个或者所有的变量和函数,另一个代码文件能够导入到本人的文件中,实现变量及函数的复用。

node 的导出

node 的导出有两种形式,一种是module.exports,另一种是exports。这两个对象都是全局内置的,能够间接应用,他们的用法如下

// 你能够将变量独自一个一个的导出
exports.a = "a";
exports.b = 123;
exports.fn = function() {console.log('我是一个函数,而且还是匿名的')
}

// 切记,这样写是不行的,具体起因稍后解释
exports = {
    a,
    b
}

// 也能够一起导出
let c = true;
let fn2 = function(){console.log('我是一个函数,而且还是匿名的') 
}

module.exports = {
    c,
    fn2
}

你可能会感觉 module.exportsexports很像, 事实上他们的确有关系,module.exportsexports 援用的是同一个对象,也就是说 exports.a 等同于module.exports.a

具体点说就是 module.exports 是一个对象,node 会将它外面的属性和办法都公开,供其余模块导入,而 exports 是指向 module.exports 的变量,exports.xx其实就是module.exports.xx

同时这也解释了后面为什么不能间接让 exports = xx,因为这样会扭转exports 本来的指向。

node 的导入

既然有导出,天然就有导入,nodejs 模块通过调用 require() 实现对其余模块数据的导入。

你可能看到过以下这两种类型的导入形式

const fs = require('fs');

const user = require('./user.js')

第一种是导入 nodejs 内置的 fs 模块,无需写门路,而第二种是导入用户本人写的模块,因而要写门路(门路能够是相对路径,也能够是绝对路径)。

模块的分类

事实上,后面咱们说的 node 内置模块也称为 外围模块,它在 nodejs 源代码编译的时候就会被编译成二进制文件,nodejs 启动时,这些外围模块就会间接被加载进内存,所以外围模块加载时,绝对于文件模块,外围模块援用时不须要进行文件定位和动静编译,速度上有劣势,导入时间接写模块名,不必填写门路,如 http,fs,path 等罕用模块都属于外围模块。

而用户本人写的代码模块叫做 文件模块,文件模块也依据导入的形式不同分为 <u> 门路模式引入的模块 </u> 和 <u> 自定义模块 </u>。

const express = require('express');// 自定义模块

const usersRouter = require('./routes/users');// 以门路模式引入的模块

对于门路模式引入的模块,因为提供了确切的门路,引入时 require 办法会把指定的门路转化为硬盘上的实在门路,并用这个门路作为索引将编译后的后果进行缓存。因为指定了门路,这种模式的文件模块在路径分析时能够节俭大量的工夫,其速度比自定义模块要快,然而比外围模块要慢

  • 外围模块
  • 文件模块

    • 以门路导入的模块
    • 自定义模块

路径分析

对于自定义模块,自定义模块会遵循以下策略进行 路径分析,这会消耗大量工夫。

  1. 查找当前目录下的 node_modules 目录,看是否有匹配项
  2. 查找父级目录下的 node_modules 目录,看是否有匹配项
  3. 依照这个规定始终往父目录搜寻直到到根目录下的 node_modules

文件定位

当实现路径分析之后,导入的门路没有文件扩展名,node 会对文件进行扩展名进行剖析,会依照.js,.node,.json 这个程序一一进行尝试。

如果门路指向的不是一个文件,而是一个目录,那么:

  • 首先会在命中的目录下寻找 package.json 这个文件并用 JSON.parse 进行解析,取出 json 文件中 main 属性的值,作为命中的文件
  • 如果找不到 package.json 或者对应的 main 属性,那么会用这个目录上面 index 文件作为命中文件,仍旧是依照.js,.node,.json 这个程序一一进行尝试
  • 如果仍旧找不到 index,那么此次文件定位失败,将会依照下面提到的门路遍历规定,往上一级持续寻找

因为一层一层的查找,自定义模块的路径分析须要消耗大量的事件,会导致搜寻效率较为低下,所以自定义模块的加载性能要比以门路模式加载的形式要慢。

缓存

另外,node 会将导入过的模块进行 缓存,下一次援用时,会先查看缓存中有没有对应的文件,优先从缓存中进行加载,缩小不必要的耗费。

总结

因而,node 能够用 module.exportsexports进行导出操作,其中 module.exports 指向的是要导出的对象,而 exports 是对 module.exports 的指向;node 通过 require() 进行导入操作,导入的形式能够是有门路的,也能够是无门路的(外围模块,自定义模块)。

node 的模块分以下几类:

  • 外围模块
  • 文件模块

    • 以门路导入的模块
    • 自定义模块

加载速度:缓存 > 外围模块 > 以门路导入的模块 > 自定义模块

ES6 的模块化

ES6 为 JavaScript 增加了 importexport关键字,将模块化作为外围语言个性来反对了。
ES6 Module 在概念上和 CommonJs 基本相同,都是将代码拆分到不同的代码文件中,每个代码文件都是一个模块,模块之间能够相互导入和导出。

ES6 Module 根本应用

// a.js
export a = 'a';

export fn1(){console.log("hello ES6")
}

// 和 CommonJs 一样,es6 Module 也能够一起导出
let b = 1;
let fn2 = function(){console.log("hello ES6 模块")
}

export {b,fn2}
// b.js
import a from "./a.js"

console.log(a.a);

a.fn1()  // hello ES6

留神:export {b,fn2}看似如同是申明了一个对象字面量,然而实际上这里的花括号并不会定义对象字面量,这种导出语法仅仅是要求在一对花括号中给出一个逗号分隔的列表。

另外,ES6 的模块会主动采纳严格模式,不论你有没有在模块头部加上"use strict";

扩大

除了下面的根本用法外,importexport 还有一些扩大应用办法。

使用解构

import {a} from "./a.js"console.log(a) // 等于 import a from "./a.js"console.log(a.a) 

通过 as 关键字重命名

import {a as b} from "./a.js"console.log(b) // 等于 import {a} from "./a.js"console.log(a) // 也能够在导出时设置别名 export {a as b}

执行一个模块,然而不导入任何值能够这样写

import "./a.js"

如果多次重复执行同一句 import 语句,那么只会执行一次,而不会执行屡次。

整体加载,即用星号(*)指定一个对象,所有输入值都加载在这个对象下面。

import * as moduleA from './a.js';console.log(moduleA.a)  // 'a'

从下面的例子来看,导入须要先晓得导出的函数 / 变量名,能力应用,这样并不是很不便,咱们能够设置一个默认值。

// moduleA.jsexport default function () {  console.log('foo');}

这样,导入的时候不须要晓得 moduleA 导出的函数名或变量名。

// moduleB.js// 能够命名为任意名称 import fn from "./moduleA"fn() // foo

下面代码的 import 命令,能够用任意名称指向 moduleA.js 输入的办法,这时就不须要晓得原模块输入的函数名。须要留神的是,这时 import 命令前面,不应用大括号。

留神:一个模块中 export default 只能存在一个

实质上,export default就是输入一个叫做 default 的变量或办法,而后零碎容许你为它取任意名字。所以,上面的写法也是无效的。

// modules.jsfunction add(x, y) {return x * y;}export {add as default};

export 与 import 的复合写法

如果在一个模块之中,先输出后输入同一个模块,import语句能够与 export 语句写在一起。

export {foo, bar} from 'my_module';// 能够简略了解为 import {foo, bar} from 'my_module';export {foo, bar};

更多复合写法见网道 -ES6

动态导入和动静导入

后面应用的 import 导入具备以下特点:

  1. 存在晋升行为,会晋升到整个模块的头部,首先执行
  2. 导入的变量和函数都是只读的,因为它的实质是输出接口
  3. 编译时导入

importexport 命令只能在模块的顶层,不能在代码块之中(比方,在 if 代码块之中,或在函数之中),因为引擎解决 import 是在编译阶段,在代码执行前就先将模块内容导入,这时不会去剖析或执行 if 语句。

// 报错 if (true) {import moduleA from "./a.js"}

正是因为 import 是动态导入,所以要导入的模块是编写代码时就确定了的,无奈在代码执行时决定。

// 报错 const path = './' + fileName;import a from path;

为了解决这个问题,ES2020 提案 引入 import() 函数,反对动静加载模块。

// 下面的例子能够这样写 const path = './' + fileName;import(path).then(module=>{}).catch(err=>{});

importimport() 最大的区别就是前者是动态导入,后者是动静导入,并且后者返回的是一个 Promise。

import()函数能够用在任何中央,不仅仅是模块,非模块的脚本也能够应用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有动态连贯关系,这点也是与 import 语句不雷同。import()相似于 Node 的 require 办法,区别次要是前者是异步加载,后者是同步加载。

import()适宜:

  • 按需加载
  • 条件加载
  • 动静门路加载

import()还能够配合 解构 asyn/await应用。

import('./myModule.js').then(({export1, export2}) => {// ...·});async function main() {  const myModule = await import('./myModule.js');  const {export1, export2} = await import('./myModule.js');  // 同时加载多个模块  const [module1, module2, module3] =    await Promise.all([import('./module1.js'),      import('./module2.js'),      import('./module3.js'),    ]);}main();

CommonJs vs Es6 Module

CommonJs Es6 Module
反对 node 程序 反对 web,将来也将反对 node
CommonJs 能够动静加载语句,代码产生在运行时 Es Module 既能动静导入,也能动态导入
CommonJs 导出值是拷贝,能够批改导出的值 ES6 Module 输入的是值的援用,并且是只读的
CommonJs 会缓存导入的模块 不会缓存值

写到最初

随着官网将模块化作为外围语言个性来反对,将来的模块化计划很可能会是对立应用 ES6 Module(Node13 开始反对 ES6 模块),然而因为目前绝大多数 node 程序都应用 CommonJs,将来较长一段时间可能依然是两种模块计划并行应用。

最初,码字不易,如果这篇文章对你有帮忙,还请点个赞,谢谢。

参考

局部内容参考自:

网道 -ES6 Module

《JavaScript 权威指南第七版》

退出移动版