随着我们的应用越来越大,我们希望将其拆分成多个文件,即所谓的“模块”。模块可以包含用于特定目的的类或函数库。很长一段时间,JavaScript没有语言级别的模块语法。这不是问题,因为原始脚本又小又简单,因此无需对其进行模块化。但最终脚本变得越来越复杂,因此社区发明了很多将代码组织成模块的方法,使用特殊的库按需加载模块。仅举几例(出于历史原因):AMD-最古老的模块系统之一,最初由require.js库实现。CommonJS-为Node.js服务器创建的模块系统。UMD-另一个模块系统,作为通用模块系统提出,它与AMD和CommonJS兼容。现在,它们都在慢慢成为历史的一部分,但我们仍然可以在古老的剧本中找到它们。语言级别的模块系统出现在2015年的标准(ES6)中,此后逐渐演进,目前已被各大浏览器和Node.js所支持。所以,我们将从现在开始学习现代JavaScript模块。1.什么是模块?一个模块就是一个文件。脚本是一个模块。就这么简单。模块可以相互加载,并且可以使用特殊指令export和import交换一个模块的功能并从另一个模块调用函数:export关键字标记可以从当前模块外部访问的变量和函数。import关键字允许从其他模块导入功能。例如,我们有一个导出函数的sayHi.js文件://sayHi.jsexportfunctionsayHi(user){alert(`Hello,${user}!`);}...然后另一个文件可能会导入并使用这个函数://main.jsimport{sayHi}from'./sayHi.js';alert(sayHi);//function...sayHi('John');//你好,约翰!导入指令是通过相对于当前文件路径./sayHi.js加载模块,并将导入的函数sayHi赋值给对应的变量。让我们在浏览器中运行这个例子。由于模块支持特殊的关键字和函数,我们必须使用浏览器会自动获取和解析(评估)导入的模块(如有必要,解析模块的导入),然后运行脚本。模块只能通过HTTP(s)工作,而不是本地文件。如果您尝试通过file://协议在本地打开网页,您会发现导入/导出指令不起作用。您可以使用本地Web服务器(例如静态服务器)或使用编辑器的“实时服务器”功能(例如VSCode的实时服务器扩展)来测试模块。2.模块核心功能模块与“常规”脚本有何不同?以下是一些适用于浏览器和服务器端JavaScript的核心函数。3.始终使用“usestrict”模块默认始终使用usestrict,例如,给未声明的变量赋值会产生错误(译注:在浏览器控制台可以看到错误信息)。4.模块级作用域每个模块都有自己的顶级作用域。换句话说,一个模块中的顶层变量和函数在其他脚本中是不可见的。在下面的示例中,我们导入了两个脚本,hello.js尝试使用user.js中声明的变量user但失败了:scripttype="module"src="hello.js">模块应该导出它们希望外部访问的内容,并导入它们需要的内容。因此,我们应该将user.js导入hello.js并从中获取我们需要的功能,而不是依赖全局变量。这是正确的变体:import{user}from'./user.js';document.body.innerHTML=user;//John在浏览器中,每个如果我们真的需要创建一个窗口级全局变量,我们可以将其显式分配给窗口并作为window.user访问它。但这需要你有足够充分的理由,否则不要去做。5.模块代码只在第一次导入时解析。如果同一个模块导入到其他多个位置,它的代码只会在第一次导入时执行,然后导出(导出)的内容将提供给所有导入器。这具有重要意义。让我们通过例子来看:首先,如果在一个模块中执行代码有副作用,比如显示一条消息,那么多次导入它只会触发一次显示——第一次://alert.jsalert("Moduleisevaluated!");//在不同的文件中导入相同的模块//1.jsimport`./alert.js`;//Moduleisevaluated!//2.jsimport`./alert.js`;//(什么都不显示)Inactual在开发中,顶层模块代码主要用于初始化、内部数据结构的创建,如果我们希望某些东西是可重用的——导出它。下面是一个高级点的例子。我们假设一个模块导出一个对象://admin.jsexportletadmin={name:"John"};如果这个模块被导入到多个文件中,模块只会在第一次导入时被解析,并创建admin对象,然后传递给所有导入。所有导入只获得一个唯一的管理对象://1.jsimport{admin}from'./admin.js';admin.name="Pete";//2.jsimport{admin}from'./admin.js';alert(admin.name);//Pete//1.js和2.js导入同一个对象//在1.js中对对象所做的修改在2.js中也是可见的所以,我们重申一下——模块只执行一次。生成一个导出,然后与它的所有导入共享,所以如果管理对象在某处被修改,其他模块也可以看到修改。这种行为允许我们在第一次导入时设置模块。我们只需要设置一次它的属性,然后就可以直接用于进一步的导入。例如,以下admin.js模块可能提供特定功能,但希望凭据从外部进入管理对象://admin.jsexportletadmin={};exportfunctionsayHi(){alert(`Readytoserve,${admin.name}!`);}在init.js——我们APP的第一个脚本中,设置admin.name。现在它随处可见,包括在admin.js中调用。//init.jsimport{admin}from'./admin.js';admin.name="Pete";另外一个模块也可以看到admin.name://other.jsimport{admin,sayHi}from'./admin.js';alert(admin.name);//PetesayHi();//准备好了,Pete!6.import.metaimport.meta对象包含有关当前模块的信息。它的内容取决于它的环境。在浏览器环境中,它包含当前脚本的URL,或者如果它是在HTML中,则包含当前页面的URL。7.在一个模块中,“this"是undefined这是一个小特性,但为了完整起见,我们应该提及它。在模块中,顶层this是未定义的。将this与非模块脚本进行比较,可以发现非模块脚本的顶级this是全局对象:八、浏览器特有的功能与常规脚本相比,带有type="module"标志的脚本有一些浏览器特有的差异。如果您是第一次阅读本文或者您不打算在浏览器中使用JavaScript,则可以跳过此部分。9.模块脚本延迟模块脚本总是延迟的,defer特性(在脚本:async,defer章节中描述)对外部脚本和内联脚本的影响是一样的。即:下载外部模块脚本对比下面的正则脚本:
