当前位置: 首页 > news >正文

再苦再累也必须要弄懂的:ES6的ES Module

再苦再累也必须要弄懂的:ES6的ES Module

Introduciton

  • 今天就来讲一讲,ES6 的模块化规范 ES Module。

  • 什么是模块化?

    百度百科解释道:模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

    我的理解:将代码按照 功能,作用,类别等,划分成一个个独立的文件,每个文件可以看做一个模块。

  • ES6 提供的模块化方案叫做 ES Module,简称 esm

  • 早期的 Javascript 是没有模块化的概念,如果想利用 Javascript 构建一个大型项目,就会有很多问题。例如 1.命名冲突;2.变量私有;2.依赖关系的管理等问题。

  • 在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

运行ES Module规范的 js 文件

在讲 ES Module 具体的知识点之前,必须要先讲,如何执行我们的 ES Module 的代码,方便后续我们调试。

运行代码主要两个环境:浏览器和 NodeJs,我在这里分别做一下说明。

浏览器环境

前置逻辑

正常的情况,我们可以在 html 页面中,直接使用 script 标签引入我们的 js 文件,如下述案例所示。

<script src="./main.js"></script>
<!-- <script type="application/javascript" src="./main.js"></script> -->

script 标签有一个 type 属性,默认情况为:“application/javascript”。所以大多数情况都简写了。但是在 ES Module 中,为了告诉浏览器我们是用的 ES Module,需要修改 type属性为 “module”。

如下所示:

<script src="./main.js" type="module"></script>

演示具体代码:

错误的情况:

// main.js
/* 使用了 ES6 的命令 export */
export var a = 1
<body>
  <script src="./main.js"></script>
  /* 不写type属性的情况,会直接报错 */
  // Uncaught SyntaxError: Unexpected token 'export' (at main.js:1:1)
</body>

正确的情况:

<body>
  <script src="./main.js" type="module"></script>
</body><body>
  <script type="module">
    import { a } from './main.js'
    console.log(a) // 1
  </script>
</body>

本地直接打开 带有 <script type="module"> 的 html 页面,会有跨域问题。

  • 解决方式:可以使用 (VSCode的插件:liveServer) 创建本地服务,打开此 html 页面即可。

  • 跨域的原因:我猜测可能是因为 type=“module” 是异步加载所导致的。

NodeJs环境

NodeJS 本身的模块化标准是 CommonJs。如何让 NodeJS 支持运行 ES Module?

前提:NodeJs 版本要求

NodeJs的8.9之后的版本,就开始支持 ES6了 ,但是在 13.2 版本之后才开启 默认支持运行 ES Module。所以使用版本在 8.9~13.2 之间的 NodeJs ,执行 ES Module 的 js文件。需要添加配置项 --experimental-modules,开启 默认关闭的 ES Module 支持。

方式一: .mjs

修改 js 文件后缀 由 .js.mjs

演示:

// main.mjs
/* 后缀名设置为 mjs */
export var a = 1

// CMD 中的输入命令
node ./main.mjs

// 8.9~13.2 之间版本的 NodeJs
node --experimental-modules ./main.mjs

方式二:"type": "module"

配置项目的 package.json 中的 type 属性为 module。

演示:

// package.json
/* type 设置为 module */
"type": "module"


// CMD 中的输入命令
node ./main.js

// 8.9~13.2 之间版本的 NodeJs
node --experimental-modules ./main.js
  • 对标 ES Module,有时候遇到 .cjs 结尾的文件,其实也是js文件,不过它是 CommonJS 规范。

  • package.json 中的 type 设置为 commonjs,可以表示使用 CommonJS 规范。

第一条:严格模式

Es Module 第一个特点:默认开启了严格模式。即便你没有在文件的开头添加 “use strict”;

既然开启了严格模式,严格模式限制有哪些?

我第一反应:this 指向不再是指向 window ,而是 undefined

主要有以下限制:

  1. 变量必须声明后再使用
  2. 函数的参数不能有同名属性,否则报错
  3. 不能使用 with 语句
  4. 不能对只读属性赋值,否则报错
  5. 不能使用前缀 0 表示八进制数,否则报错
  6. 不能删除不可删除的属性,否则报错
  7. 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  8. eval 不会在它的外层作用域引入变量
  9. eval 和 arguments 不能被重新赋值
  10. arguments 不会自动反映函数参数的变化
  11. 不能使用 arguments.callee
  12. 不能使用 arguments.caller
  13. 禁止 this 指向全局对象
  14. 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  15. 增加了保留字(比如 protected、static 和 interface)

第二条:export

模块中的变量是私有的,外部是无法直接访问的,我们可以通过 export 主动向外输出变量,供外部模块使用。

export 英文释义:导出,输出;(我习惯理解为输出的意思)

!!!export这个单词不要混淆,需要和CommonJS 的 exportsmodule.exports作区分

说个我自己用来记忆理解:CommonJS 的输出,默认都是放在一个对象中输出, 对象存储的信息比较多,所以需要加 s 后缀。

export 的使用案例

1. 普通输出

export var tomato = 'sweet'

export var say = function () {
  console.log('说话')
}

2. 批量输出

var a = 1
var b = 2

export { a, b }
// 这种方式更好,可以很容易知道输出了哪些变量。

3.输出别名

var n = 1
export { n as m }
// 实际上输出的名称为 m。但是取值是取 n 的值。

export 的注意事项

1.错误的导出

// 错误的输出
export 1

// 错误的输出
var num = 1
export num

上面的示例会报错, why ?
正确的导出,实质上是,在接口名与模块内部变量之间,建立了一一对应的关系。
我自己的理解:使用 export 命令输出,必须建立 输出的变量名 和 内部的变量之间的关系,才可以输出

2. export import不允许在块级作用域中使用

if (true) {
  // 报错
  export var a = 1
}

ES6 的 export import 不允许在块级作用域中使用。

第三条:import

说完export,说说 import;既然模块的变量可以导出,那么我们如何引入这些变量?这就需要命令: import

import 英文释义:进口,进口商品;输入,引进 (我习惯理解为输入的意思)

前置条件

为了方便演示 import ,加上我本地使用 node 调试代码比较方便。我先定义一个导出的文件 a.mjs

后续没有明确说明的。默认都是导入下方的 a.mjs ,并且采取 node ./main.mjs 的方式运行示例代码。

export var tomato = 'sweet'

export var say = function () {
  console.log('说话')
}

var a = 1
var b = 2

export { a, b }

import 的使用案例

1.直接引入 a.mjs 中的变量a

import { a } from './a.mjs'
console.log(a) // 1

2.引入 a.mjs 中的变量 b 但是取别名为 newB

import { b as newB } from './a.mjs'
console.log(newB) // 2
// console.log(b) // 直接报错: b is not defined

3.引入 a.mjs 所有的输出到 obj 中

import * as obj from './a.mjs'
obj.say() // 说话
console.log(a,b) // 1 2

4.执行块a.mjs 但不引入变量

import './a.mjs'

import 的注意事项

1. import 的变量是只读的

import { a } from './a.mjs'
console.log(a) // 1

a = 2 // TypeError Assignment to constant variable.

2. import 的变量实际上是对原本模块中变量的引用

// a.mjs
export var num = 1
export function add() {
  num++
}

// main.mjs
import * as obj from './a.mjs'
console.log(obj.num)
// 1
obj.add()

console.log(obj.num)
// 2

/* 这里可以发现,import引入的变量其实是对原本模块变量的一个链接引用,当原模块变量值改变的时候,我们引入的变量的值也会跟着改变 */

3. 和 export 同理, import 语句不允许放在块级作用域中使用,会直接报错;

4. 由于ES Module是静态编译,所以 import会被提升到最顶部执行 ;

// a.mjs
console.log('a.mjs开始执行啦')
export var num = 1


// main.mjs
console.log('main.js开始执行了')

import * as obj from './a.mjs'
console.log(obj.num)

console.log('main.js结束执行了')

// a.mjs开始执行啦
// main.js开始执行了
// 1
// main.js结束执行了

/* 可以看到,优先执行了import * as obj from './a.mjs', 随即执行了 a.mjs*/

5. import 的执行逻辑 优先深度遍历,先子后父;

// a.mjs
console.log('a开始执行啦')
import { say } from './b.mjs'
import { edit } from './c.mjs'
export var a = 1
console.log('a结束了')

// b.mjs
console.log('b开始执行啦')
export function say() {
  console.log('开始说话')
}
import { a } from './a.mjs'
console.log(a)
console.log('b结束了')

// c.mjs
console.log('c开始执行啦')
export function edit() {
  console.log('开始编辑')
}
console.log('c结束了')

// b开始执行啦
// undefined
// b结束了
// c开始执行啦
// c结束了
// a开始执行啦
// a结束了

6. 多次重复执行同一句 import 语句,只会执行一次;

// a.mjs
console.log('执行文件a')
export var tomato = 'sweet'

// main.mjs
import './a.mjs'
import './a.mjs'
// 执行文件a

/* 不管我引入了多少次 a.mjs ,只会打印一次 执行文件a */

第四条:默认导出 和 默认引入

上面的实例,通过 import 和 export 就可以顺利的 输出和输入变量。

但是还有一个问题,对于使用者来说,拿到一个模块,有些时候我们并不知道,这个模块输出了什么。此时 ES6 提供了默认的导出和默认的引入来解决这种困扰。

默认导出和默认输出 的使用案例

// a.mjs
export default 1

// main.mjs
import a from './a.mjs'
console.log(a)
// 1

默认导出和默认输出 的注意事项

1. export default 和 export的区别

/* 默认导出和默认输出 */
// a.mjs
export default 1

// main.mjs
import a from './a.mjs'
console.log(a)
// 1


/* 普通的写法 */
// a.mjs
export var a = 1

// main.mjs
import { a } from './a.mjs'
console.log(a)
// 1

为什么 export default 不使用 var 声明一个变量导出呢?

原因是 export default 可以看做就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。

下面看示例:2. export default 的本质

2. export default 的本质

/* export default */
// a.mjs
export default 1

// main.mjs
import a from './a.mjs'
console.log(a)
// 1

/* 模拟 default*/
// a.mjs
var a = 1
export {a as default}


import a  from './a.mjs'
console.log(a)
// 1

可以看到上述代码 ,使用 export {a as default} ,效果是和 export default 是相同的。

3. 又想引入部分变量,又想引入默认的导出?

// a.mjs
export default function () {
  console.log('你好呀')
}
export var a = 1
export var b = 2


// main.mjs
import xx, { a, b } from './a.mjs'
xx() // 你好呀
console.log(a, b) // 1 2

/* 需要注意:这里的xx可以任意替换 */ 

第五条:import()

由于 ES Module 是静态编译,提前异步加载,所以不能在块级作用域中使用 import export;

但是有这么几个问题:

  • 一方面 CommonJS 的 require 是运行时加载(ES Module 若要支持 CommonJS,这个运行时就需要支持)。

  • 一方面,有时候确实希望根据代码逻辑,去引入文件。

ES2022 引入了 import(),可以叫做 动态 import;

import() 的使用

// a.mjs
export default function () {
  console.log('你好呀')
}

export var a = 1

// main.js
console.log('main.js开始执行了')
setTimeout(() => {
  import('./a.mjs')
    .then((res) => {
      console.dir(res)
    })
    .catch((err) => {
      console.log(err)
    })
}, 200)

// main.js开始执行了
// [Module: null prototype]   { a: 1, default: [Function: default] }

在这里插入图片描述

import() 的注意事项

1. import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。

2. import() 函数返回值是一个 Promise 对象;

3. import() 适用场景

  • 条件加载

    if (xxx) {
     import('./xxx')
    }
    
  • 路由懒加载

    const UserDetails = () => import('./views/UserDetails.vue')
    
  • 按需加载

    xxx.onClick = function () {
      import('./xxx')
    }
    

<script> 加载文件的顺序

前面有说到 <script> 标签,这个章节说一下 <script> 加载文件的顺序 。

1. 默认情况

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

2. 异步加载

<script src="xxx.js" defer></script>
<script src="xxx.js" async></script>

添加属性,defer 或者 async,都会开启异步加载

deferasync的区别是:

  • defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;
  • async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

一句话,defer是“渲染完再执行”,async是“下载完就执行”。

另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

3. <script type="module" src="./foo.js"></script> 加载文件的逻辑

有上述的解释可以得到如下结论:

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

所以 <script type="module" src="./foo.js"></script> 可以理解为,异步加载 ./foo.js 文件,等页面渲染完毕之后再开始执行我们的脚本。

当然也可以给<script>标签加上 async 标志,使它一旦下载完,就中断渲染,执行这个脚本。

<!-- 例如: -->
<script type="module" src="./foo.js" async></script>

总结

总结一下学到的知识。

  1. ES Module 是 ES6提供的模块化标准;

  2. ES Module 基础知识主要包含这几点:

    • 默认开启严格模式;
    • import
    • export
    • default export

参考文章

  • ECMAScript 6 入门-阮一峰_Module 的语法

end

  • 本文主要记录了我学习到的 ES Module,若有错误,欢迎指出不胜感激。
  • 本文还缺少了 ES Module 和我们常用的 CommonJS 的差异比较。我打算学习完 CommonJS 之后,再对比一下两者的差异。
  • 加油!!!

相关文章:

  • K210使用Mx-yolov3训练
  • Springboot中日志的简单使用
  • 0. SQL细节要点
  • 网络安全——Cobaltstrike
  • 架构师的 36 项修炼第07讲:高性能系统架构设计
  • 微信小程序开发03 自定义组件:怎么培养组件化思维?
  • 4.bs4 节点遍历
  • 基于ssm+vue+elementui的二手车交易管理系统
  • 计算机毕业设计springboot+vue基本微信小程序的水库巡检系统
  • 3.BeautifulSoup库
  • 8.cookie的获取
  • 商标注册需要什么材料
  • 什么是布隆过滤器
  • 什么是Quartz
  • 【golang】认识 Go 并发+
  • AHK 中 = 和 == 等比较运算符的用法
  • Android交互
  • Apache的基本使用
  • ES6之路之模块详解
  • git 常用命令
  • Java编程基础24——递归练习
  • Java的Interrupt与线程中断
  • js作用域和this的理解
  • vue-cli3搭建项目
  • 分享自己折腾多时的一套 vue 组件 --we-vue
  • 看域名解析域名安全对SEO的影响
  • 利用DataURL技术在网页上显示图片
  • 两列自适应布局方案整理
  • 如何利用MongoDB打造TOP榜小程序
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 项目实战-Api的解决方案
  • #在线报价接单​再坚持一下 明天是真的周六.出现货 实单来谈
  • (02)vite环境变量配置
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (20050108)又读《平凡的世界》
  • (C语言)字符分类函数
  • (八十八)VFL语言初步 - 实现布局
  • (六)库存超卖案例实战——使用mysql分布式锁解决“超卖”问题
  • (续)使用Django搭建一个完整的项目(Centos7+Nginx)
  • (一)Mocha源码阅读: 项目结构及命令行启动
  • (转)拼包函数及网络封包的异常处理(含代码)
  • *setTimeout实现text输入在用户停顿时才调用事件!*
  • .NET Standard、.NET Framework 、.NET Core三者的关系与区别?
  • .net 调用php,php 调用.net com组件 --
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • @ComponentScan比较
  • @EventListener注解使用说明
  • @transactional 方法执行完再commit_当@Transactional遇到@CacheEvict,你的代码是不是有bug!...
  • @我的前任是个极品 微博分析
  • [APUE]进程关系(下)
  • [BUUCTF NewStarCTF 2023 公开赛道] week4 crypto/pwn
  • [C++] 如何使用Visual Studio 2022 + QT6创建桌面应用
  • [C++从入门到精通] 14.虚函数、纯虚函数和虚析构(virtual)
  • [CLR via C#]11. 事件
  • [JavaWeb学习] idea新建web项目