文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

JavaScript引入模块的历史简介

2024-12-03 15:08

关注

 随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块(module)”。一个模块可以包含用于特定目的的类或函数库。

很长一段时间,JavaScript 都没有语言级(language-level)的模块语法。这不是一个问题,因为最初的脚本又小又简单,所以没必要将其模块化。

但是最终脚本变得越来越复杂,因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块。

列举一些(出于历史原因):

现在,它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们。

语言级的模块系统在 2015 年的时候出现在了标准(ES6)中,此后逐渐发展,现在已经得到了所有主流浏览器和 Node.js 的支持。因此,我们将从现在开始学习现代 JavaScript 模块(module)。

一、什么是模块?

一个模块(module)就是一个文件。一个脚本就是一个模块。就这么简单。

模块可以相互加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数:

export 关键字标记了可以从当前模块外部访问的变量和函数。

import 关键字允许从其他模块导入功能。

例如,我们有一个 sayHi.js 文件导出了一个函数:

  1. //  sayHi.js 
  2. export function sayHi(user) { 
  3.   alert(`Hello, ${user}!`); 

 ……然后另一个文件可能导入并使用了这个函数: 

  1. //  main.js 
  2. import {sayHi} from './sayHi.js'
  3.  
  4. alert(sayHi); // function... 
  5. sayHi('John'); // Hello, John! 

 import 指令通过相对于当前文件的路径 ./sayHi.js 加载模块,并将导入的函数 sayHi 分配(assign)给相应的变量。

让我们在浏览器中运行一下这个示例。

由于模块支持特殊的关键字和功能,因此我们必须通过使用 

浏览器会自动获取并解析(evaluate)导入的模块(如果需要,还可以分析该模块的导入),然后运行该脚本。

模块只通过 HTTP(s) 工作,在本地文件则不行

如果你尝试通过 file:// 协议在本地打开一个网页,你会发现 import/export 指令不起作用。你可以使用本地 Web 服务器,例如 static-server,或者使用编辑器的“实时服务器”功能,例如 VS Code 的 Live Server Extension 来测试模块。

二、模块核心功能

与“常规”脚本相比,模块有什么不同呢?

下面是一些核心的功能,对浏览器和服务端的 JavaScript 来说都有效。

三、始终使用 “use strict”

模块始终默认使用 use strict,例如,对一个未声明的变量赋值将产生错误(译注:在浏览器控制台可以看到 error 信息)。

  1. "module"
  2.   a = 5; // error 
  3.  

 四、模块级作用域

每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。

在下面这个例子中,我们导入了两个脚本,hello.js 尝试使用在 user.js 中声明的变量 user,失败了:

  1.  
  2. "module" src="user.js"
  3. "module" src="hello.js"

 模块期望 export 它们想要被外部访问的内容,并 import 它们所需要的内容。

所以,我们应该将 user.js 导入到 hello.js 中,并从中获取所需的功能,而不要依赖于全局变量。

这是正确的变体:

  1. import {userfrom './user.js'
  2.  
  3. document.body.innerHTML = user; // John 

 在浏览器中,每个  

  •  
  • "module"
  •   alert(user); // Error: user is not defined 
  •  
  •  如果我们真的需要创建一个 window-level 的全局变量,我们可以将其明确地赋值给 window,并以 window.user 来访问它。但是这需要你有足够充分的理由,否则就不要这样做。

    五、模块代码仅在第一次导入时被解析

    如果同一个模块被导入到多个其他位置,那么它的代码仅会在第一次导入时执行,然后将导出(export)的内容提供给所有的导入(importer)。

    这有很重要的影响。让我们通过示例来看一下:

    首先,如果执行一个模块中的代码会带来副作用(side-effect),例如显示一条消息,那么多次导入它只会触发一次显示 —— 即第一次:

    1. //  alert.js 
    2. alert("Module is evaluated!"); 

    1. // 在不同的文件中导入相同的模块 
    2.  
    3. //  1.js 
    4. import `./alert.js`; // Module is evaluated! 
    5.  
    6. //  2.js 
    7. import `./alert.js`; // (什么都不显示) 

     在实际开发中,顶级模块代码主要用于初始化,内部数据结构的创建,并且如果我们希望某些东西可以重用 — 请导出它。

    下面是一个高级点的例子。

    我们假设一个模块导出了一个对象:

    1. //  admin.js 
    2. export let admin = { 
    3.   name"John" 
    4. }; 

     如果这个模块被导入到多个文件中,模块仅在第一次被导入时被解析,并创建 admin 对象,然后将其传入到所有的导入。

    所有的导入都只获得了一个唯一的 admin 对象:

    1. //  1.js 
    2. import {admin} from './admin.js'
    3. admin.name = "Pete"
    4.  
    5. //  2.js 
    6. import {admin} from './admin.js'
    7. alert(admin.name); // Pete 
    8.  
    9. // 1.js 和 2.js 导入的是同一个对象 
    10. // 在 1.js 中对对象做的更改,在 2.js 中也是可见的 

     所以,让我们重申一下 —— 模块只被执行一次。生成导出,然后它被分享给所有对其的导入,所以如果某个地方修改了 admin 对象,其他的模块也能看到这个修改。

    这种行为让我们可以在首次导入时 设置 模块。我们只需要设置其属性一次,然后在进一步的导入中就都可以直接使用了。

    例如,下面的 admin.js 模块可能提供了特定的功能,但是希望凭证(credential)从外部进入 admin 对象:

    1. //  admin.js 
    2. export let admin = { }; 
    3.  
    4. export function sayHi() { 
    5.   alert(`Ready to serve, ${admin.name}!`); 

     在 init.js 中 —— 我们 APP 的第一个脚本,设置了 admin.name。现在每个位置都能看到它,包括在 admin.js 内部的调用。

    1. //  init.js 
    2. import {admin} from './admin.js'
    3. admin.name = "Pete"

     另一个模块也可以看到 admin.name:

    1. //  other.js 
    2. import {admin, sayHi} from './admin.js'
    3.  
    4. alert(admin.name); // Pete 
    5.  
    6. sayHi(); // Ready to serve, Pete! 

     六、import.meta

    import.meta 对象包含关于当前模块的信息。

    它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL。

    1. "module"
    2.   alert(import.meta.url); // 脚本的 URL(对于内嵌脚本来说,则是当前 HTML 页面的 URL) 
    3.  

     七、在一个模块中,“this” 是 undefined

    这是一个小功能,但为了完整性,我们应该提到它。

    在一个模块中,顶级 this 是 undefined。

    将其与非模块脚本进行比较会发现,非模块脚本的顶级 this 是全局对象:

    1.  
    2.  
    3. "module"
    4.   alert(this); // undefined 
    5.  

     八、浏览器特定功能

    与常规脚本相比,拥有 type="module" 标识的脚本有一些特定于浏览器的差异。

    如果你是第一次阅读或者你不打算在浏览器中使用 JavaScript,那么你可以跳过本节内容。

    九、模块脚本是延迟的

    模块脚本 总是 被延迟的,与 defer 特性(在 脚本:async,defer 一章中描述的)对外部脚本和内联脚本(inline script)的影响相同。

    也就是说: