你并不了解 JavaScript:作用域与闭包 - 第二版 - 第八章:模块化模式

第八章:模块化模式

在本章中,我们将通过探索所有编程中最重要的代码组织模式之一:模块,来结束本书的正文。正如我们将看到的那样,模块本质上是由我们已经讲过的内容构建而成:这是你学习词法作用域和闭包所付出努力的回报。

我们研究了词法作用域的各个角度,从全局作用域的广度到嵌套的块作用域,再到错综复杂的变量生命周期。然后,我们利用词法作用域来了解闭包的全部功能。

花点时间回顾一下,你在这段旅程中已经走了多远;在更深入地了解 JS 方面,你已经迈出了一大步!

本书的核心主题是,理解并掌握作用域和闭包是正确构建和组织代码的关键,尤其是决定在变量中存储信息的位置。

在最后一章中,我们的目标是了解模块如何体现这些主题的重要性,如何将它们从抽象概念提升为构建程序中具体而实用的改进。

封装和最小暴露 (POLE)

封装经常被作为面向对象 (OO) 编程的一项原则,但它的基础性和广泛适用性远不止于此。封装的目的是将信息(数据)和行为(功能)捆绑在一起或置于同一位置,共同达到一个目的。

与任何语法或代码机制无关,封装的精神可以通过一些简单的事情来实现,比如使用单独的文件来保存整个程序中具有共同目的的部分。如果我们将为搜索结果列表提供功能的所有内容都打包到一个名为 “search-list.js” 的文件中,我们就封装了程序的这一部分。

现代前端编程最近的趋势是围绕组件架构来组织应用程序,这进一步推动了封装。对于许多人来说,将构成搜索结果列表的所有内容(甚至包括代码以外的内容,包括呈现标记和样式)整合到一个单一的程序逻辑单元中,即我们可以与之交互的有形内容中,感觉很自然。然后,我们给这个集合贴上 “SearchList” 组件的标签。

另一个关键目标是控制封装数据和功能某些方面的可见性。请回顾第 6 章中的最小暴露原则 (POLE),该原则旨在防御各种作用域过度暴露的危险;这些危险同时影响变量和函数。在 JS 中,我们通常通过词法作用域的机制来实现可见性控制。

这样做的目的是将程序中相似的片段组合在一起,有选择性地限制程序对我们认为私有细节部分的访问。不被视为私有的部分则被标记为公共,整个程序都可以访问。

这种努力的自然会得到更好的代码组织。当我们知道事物的位置、清晰明确的边界和连接点时,构建和维护软件就会变得更加容易。如果我们能避免过度暴露数据和功能的隐患,也就更容易保证质量。

这些就是将 JS 程序组织成模块的一些主要好处。

什么是模块?

模块是相关数据和函数(在此通常称为方法)的集合,其特点是分为隐藏的私有细节和公共可访问的细节,通常称为「公共 API」。

模块也是有状态的:它随着时间的推移维护一些信息,以及访问和更新这些信息的功能。

注意:
模块模式的一个更广泛的关注点是,通过解耦和其他程序架构技术,全面拥抱系统级模块化。这是一个复杂的话题,远远超出了我们的讨论范围,但值得在本书之外进一步研究。

为了更好地理解什么是模块,让我们将模块的一些特征与非模块的有用代码模式进行比较。

命名空间(无状态分组)

如果你将一组相关的函数组合在一起,但不包含数据,那么你就没有真正意义上的模块封装。命名空间 (namespace) 是这种无状态函数分组的更贴切的术语:

// 命名空间,而不是模块
var Utils = {cancelEvt(evt) {evt.preventDefault();evt.stopPropagation();evt.stopImmediatePropagation();},wait(ms) {return new Promise(function c(res) {setTimeout(res, ms);});},isValidEmail(email) {return /[^@]+@[^@.]+\.[^@.]+/.test(email);},
};

这里的 Utils 是实用工具的集合,但它们都是与状态无关的函数。将功能集合在一起通常是一种好的做法,但这并不能使其成为一个模块。相反,我们定义了一个 Utils 命名空间,并在其下组织了这些函数。

数据结构(有状态分组)

即使你把数据和有状态函数捆绑在一起,如果你没有限制其中任何一个函数的可见性,那么你就没有达到封装的 POLE 层面;给它贴上模块的标签也没什么用。

想想看:

// 数据结构,而不是模块
var Student = {records: [{ id: 14, name: "Kyle", grade: 86 },{ id: 73, name: "Suzy", grade: 87 },{ id: 112, name: "Frank", grade: 75 },{ id: 6, name: "Sarah", grade: 91 },],getName(studentID) {var student = this.records.find((student) => student.id == studentID);return student.name;},
};Student.getName(73);
// Suzy

由于 records 是可公开访问的数据,而不是隐藏在公共 API 后面的数据,因此这里的 Student 并不是一个真正的模块。

Student 确实具有封装的数据和功能方面,但不具有可见性控制方面。最好给它贴上数据结构实例的标签。

模块(状态访问控制)

要充分体现模块模化的精神,我们不仅需要分组和状态,还需要通过可见性(私有与公用)进行访问控制。

让我们把上一节中的 Student 变成一个模块。我们将从一种我称之为「传统模块」的形式开始,这种模块在 2000 年代初首次出现时被称为「揭示模块」。思考一下:

var Student = (function defineStudent() {var records = [{ id: 14, name: "Kyle", grade: 86 },{ id: 73, name: "Suzy", grade: 87 },{ id: 112, name: "Frank", grade: 75 },{ id: 6, name: "Sarah", grade: 91 },];var publicAPI = {getName,};return publicAPI;// ************************function getName(studentID) {var student = records.find((student) => student.id == studentID);return student.name;}
})();Student.getName(73); // Suzy

Student 现在是一个模块实例。它有一个公共 API,只有一个方法:getName(..)。该方法可以访问隐藏的私有 records 数据。

警告:
我需要指出的是,将明确的学生数据硬编码到模块定义中只是为了说明问题。程序中的典型模块将从外部来源接收数据,通常是从数据库、JSON 数据文件、Ajax 调用等加载的数据。然后,这些数据通常会通过模块公共 API 上的方法注入模块实例。

传统模块模式是如何运作的?

请注意,模块实例是通过执行 defineStudent() IIFE 创建的。该 IIFE 返回一个对象(名为 publicAPI),该对象上有一个引用内部 getName(..) 函数的属性。

将对象命名为 publicAPI 是我的风格偏好。对象可以随心所欲地命名(JS 并不关心),也可以直接返回对象,而不将其赋值给任何内部命名变量。附录 A 将详细介绍这种选择。

从外部看,Student.getName(..) 会调用这个公开的内部函数,该函数通过闭包保持对内部 records 变量的访问。

你并不一定要返回一个以函数作为属性之一的对象。你可以直接返回一个函数来代替对象。这样仍然可以满足传统模块的所有核心要求。

根据词法作用域的工作原理,在外层模块定义函数中定义变量和函数,默认情况下会将所有内容都设为私有。只有添加到函数返回的公共 API 对象中的属性才会被导出供外部公共使用。

使用 IIFE 意味着我们的程序只需要模块的一个中心实例,也就是通常所说的「单例」。事实上,这个例子非常简单,没有明显的理由让我们只需要一个 Student 模块实例。

模块工厂(多实例)

但如果我们确实想在程序中定义一个支持多个实例的模块,我们可以对代码稍作调整:

// 工厂函数,而不是单例 IIFE
function defineStudent() {var records = [{ id: 14, name: "Kyle", grade: 86 },{ id: 73, name: "Suzy", grade: 87 },{ id: 112, name: "Frank", grade: 75 },{ id: 6, name: "Sarah", grade: 91 },];var publicAPI = {getName,};return publicAPI;// ************************function getName(studentID) {var student = records.find((student) => student.id == studentID);return student.name;}
}var fullTime = defineStudent();
fullTime.getName(73); // Suzy

我们并没有将 defineStudent() 指定为 IIFE,而是将其定义为一个普通的独立函数,在这里通常称为「模块工厂」函数。

然后,我们调用模块工厂,生成一个标为 fullTime 的模块实例。这个模块实例意味着一个新的内部作用域实例,因此,getName(..)records 拥有一个新的闭包。现在,fullTime.getName(..) 将调用该特定实例上的方法。

传统模块的定义

因此,为了澄清什么是传统模块:

  • 必须有一个外部作用域,通常是至少运行一次的模块工厂函数。
  • 模块的内部作用域必须至少有一条代表模块状态的隐藏信息。
  • 模块必须在其公共 API 中返回至少一个函数的引用,该函数对隐藏的模块状态具有闭包(以便实际保留该状态)。

我们将在附录 A 中详细介绍这种传统模块方法的其他变体。

Node CommonJS 模块

在第 4 章中,我们介绍了 Node 使用的 CommonJS 模块格式。与前面介绍的传统模块格式不同,CommonJS 模块是基于文件的;每个文件一个模块。

让我们调整一下模块示例,使其符合这种格式:

module.exports.getName = getName;// ************************var records = [{ id: 14, name: "Kyle", grade: 86 },{ id: 73, name: "Suzy", grade: 87 },{ id: 112, name: "Frank", grade: 75 },{ id: 6, name: "Sarah", grade: 91 },
];function getName(studentID) {var student = records.find((student) => student.id == studentID);return student.name;
}

recordsgetName 标识符位于本模块的顶层作用域,但这不是全局作用域(如第 4 章所述)。因此,这里的所有内容默认都是模块的私有内容。

要在 CommonJS 模块的公共 API 上公开某些内容,需要向作为 module.exports 提供的空对象添加一个属性。在一些较旧的遗留代码中,您可能会遇到对仅有的 exports 的引用,但为了代码的清晰度,您应始终使用 module. 前缀对该引用进行完全限定。

出于风格考虑,我喜欢将 “exports” 放在顶部,而将模块实现放在底部。但这些导出可以放在任何地方。我强烈建议将它们集中在一起,放在文件的顶部或底部。

有些开发人员习惯于替换默认导出为对象,就像这样:

// 为 API 定义新对象
module.exports = {// ..exports..
};

这种方法有一些怪异之处,包括多个此类模块相互循环依赖时出现意外行为。因此,我建议不要替换对象。如果你想使用对象字面风格定义一次分配多个导出,你可以这样做:

Object.assign(module.exports, {// .. exports ..
});

这里做的事情是在定义 { .. } 然后,Object.assign(..) 将所有这些属性浅拷贝到现有的 module.exports 对象上,而不是替换它。

要在模块/程序中包含另一个模块实例,请使用 Node 的 require(..) 方法。假设该模块位于 “/path/to/student.js”,我们可以这样访问它:

var Student = require("/path/to/student.js");Student.getName(73);
// Suzy

现在,Student 引用了我们示例模块的公共 API。

CommonJS 模块表现为单例,类似于之前介绍的 IIFE 模块定义风格。无论你多少次 require(..) 同一个模块,你都只会得到对单个共享模块实例的额外引用。

require(..) 是一种全有或全无的机制;它包括对模块整个公开 API 的引用。如果只想有效地访问 API 的一部分,典型的方法如下:

var getName = require("/path/to/student.js").getName;// 或者使用:var { getName } = require("/path/to/student.js");

与传统模块格式类似,CommonJS 模块 API 的公开导出方法对模块内部细节进行闭包。这就是模块单例状态在整个程序生命周期中的维护方式。

注意:
在 Node require("student") 语句中,非绝对路径("student")假定文件扩展名为 “.js”,并搜索 “node_modules”。

现代 ES 模块 (ESM)

ESM 格式与 CommonJS 格式有几处相似之处。ESM 基于文件,模块实例是单例,默认情况下所有内容都是私有的。一个显著的不同点是,ESM 文件被假定为严格模式,而无需在顶部添加 "use strict" 编译指示。没有办法将 ESM 定义为非严格模式。

与 CommonJS 中的 module.exports 不同,ESM 使用 export 关键字来公开模块的公共 API。import 关键字取代了 require(..) 语句。让我们调整 “students.js” 以使用 ESM 格式:

export { getName };// ************************var records = [{ id: 14, name: "Kyle", grade: 86 },{ id: 73, name: "Suzy", grade: 87 },{ id: 112, name: "Frank", grade: 75 },{ id: 6, name: "Sarah", grade: 91 },
];function getName(studentID) {var student = records.find((student) => student.id == studentID);return student.name;
}

这里唯一的变化是 export { getName } 语句。和以前一样,export 语句可以出现在整个文件的任何地方,但 export 必须位于顶层作用域;它不能位于任何其他代码块或函数内部。

对于如何指定 export 语句,ESM 提供了相当多的写法。例如:

export function getName(studentID) {// ..
}

尽管 export 出现在这里的 function 关键字之前,但这个表单仍然是一个 function 声明,而且恰好也是导出的。也就是说,getName标识符是函数提升(参见第 5 章),因此它在整个模块作用域内都可用。

另一种写法:

export default function getName(studentID) {// ..
}

这就是所谓的「默认导出」,其语义与其他导出不同。从本质上讲,「默认导出」是模块消费者在「导入」时的一种速记,当他们只需要这个单一的默认 API 成员时,会给他们提供一种更简洁的语法。

非默认 (default) 导出称为「命名导出」。

import 关键字与 export 关键字一样,只能在任何模块或函数之外的 ESM 顶层使用,其语法也有多种变化。第一种称为「命名导入」:

import { getName } from "/path/to/students.js";getName(73); // Suzy

如您所见,这种形式只导入模块中已明确命名的公共 API 成员(跳过任何未明确命名的成员),并将这些标识符添加到当前模块的顶层作用域中。这种导入方式对于那些习惯于使用 Java 等语言中的包导入的人来说并不陌生。

可在 { .. } 集合中列出多个 API 成员,中间用逗号隔开。还可以使用 as 关键字对已命名的导入进行重命名

import { getName as getStudentName } from "/path/to/students.js";getStudentName(73);
// Suzy

如果 getName 是模块的「默认导出」,我们可以这样导入它:

import getName from "/path/to/students.js";getName(73); // Suzy

这里唯一的区别是去掉了导入周围的 { }。如果要将默认导入与其他已命名的导入混合使用:

import { default as getName /* .. others .. */ } from "/path/to/students.js";getName(73); // Suzy

相比之下,import 的另一个主要变体称为「命名空间导入」:

import * as Student from "/path/to/students.js";Student.getName(73); // Suzy

显而易见,* 会导入所有输出到 API 的内容,包括默认内容和已命名内容,并将其存储在指定的单一命名空间标识符下。这种方法最接近 JS 历史上大多数传统模块的形式。

注意:
截至本文撰写之时,现代浏览器支持 ESM 已有数年时间,而 Node 对 ESM 的稳定支持则是最近才开始的,并且已经发展了相当长的一段时间。这种演进可能还要持续一年或更长时间;ES6 将 ESM 引入 JS 时,为 Node 与 CommonJS 模块的互操作带来了许多具有挑战性的兼容性问题。有关所有最新细节,请查阅 Node 的 ESM 文档:https://nodejs.org/api/esm.html。

周天圆满

无论是使用经典的模块格式(浏览器或 Node)、CommonJS 格式(在 Node 中)还是 ESM 格式(浏览器或 Node),模块都是结构化和组织程序功能和数据的最有效方法之一。

模块模式是我们在本书中学习如何使用词法作用域规则将变量和函数放置在适当位置的旅程的终点。POLE 是我们一贯采取的默认情况下的私有防御姿态,确保我们避免过度暴露,只与最小的公共 API 表面区域进行必要的交互。

在模块之下,利用词法作用域系统的闭包是维护所有模块状态的法宝

正文到此结束。恭喜你完成了这一段旅程!正如我在整个过程中多次说过的,暂停、思考和实践我们刚刚讨论的内容是一个非常好的主意。

当您感到舒适并准备就绪时,请查看附录,这些附录深入探讨了这些主题的一些角落,还通过一些练习题来巩固您所学的知识。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2812558.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

k8s(5)

目录 使用Kubeadm安装k8s集群: 初始化操作: 每台主从节点: 升级内核: 所有节点安装docker : 所有节点安装kubeadm,kubelet和kubectl: 修改了 kubeadm-config.yaml,将其传输给…

Azure Eventhub项目引入Servicebus报NoClassDefFoundError

前提 现有项目使用azure eventhub作为IOT数据载体进行数据传输。由于业务需要&#xff0c;需要同时引入servicebus。 <dependency><groupId>com.azure</groupId><artifactId>azure-messaging-servicebus</artifactId><version>7.13.3<…

springboot网站开发-使用MultipartFile上传图片文件到远程服务器

springboot网站开发-使用MultipartFile上传图片文件到远程服务器&#xff01;昨天上午在准备网站的一些 图片&#xff0c;下午在测试图片上传的模块&#xff0c;走了一些弯路&#xff0c;今天和大家分享一下&#xff0c;免得大家再走弯路。 首先&#xff0c;要和大家声明一件事…

vue3使用echarts绘制地图

vue3使用echarts绘制地图 安装echarts npm install echarts下载地图的json数据【我这里是把json数据单独粘出来然后新建了一个文件china.json】 下载中国及各个省份的地图数据引入 import chinaJson from ./china.json绘制地图 <template><div ref"myChart&q…

面试经典150题【31-40】

文章目录 面试经典150题【31-40】76.最小覆盖字串36.有效的数独54.螺旋矩阵48.旋转图像73.矩阵置零289.生命游戏383.赎金信205.同构字符串290.单词规律242.有效的字母异位词 面试经典150题【31-40】 76.最小覆盖字串 基本思路很简单&#xff0c;就是先移动右边到合适位置。再移…

网络安全与IP安全网络安全

网络安全与IP安全网络安全 网络安全 是指网络系统的硬件&#xff0c;软件以及系统中的数据收到的保护。 保护的基本属性为&#xff1a;机密性&#xff0c;身份认证&#xff0c;完整性和可用性&#xff1b; 基本特征&#xff1a;相对性&#xff0c;时效性&#xff0c;相关性…

[面试]我们常说的负载均衡是什么东西?

什么是负载均衡 如果用户量很多, 服务器的流量也随之增大, 此时出现两个问题, 软件性能下降 容易出现单点故障 为了解决这些问题, 引入了集群化架构, 也就是把一个软件同时部署在多个服务器上 集群化架构出现的问题 架构改变后又出现了两个问题 如何将请求均匀的发送到多…

大疆无人机视频删了怎么恢复?尝试这些恢复技巧

无人机拍摄的视频已经成为许多飞行爱好者和专业人士珍贵的记忆与资料。然而&#xff0c;误删视频是许多人都可能遇到的问题。当您不慎删除了大疆无人机中的视频时&#xff0c;不必过于焦虑。本文将为您详细介绍如何恢复这些误删的视频&#xff0c;帮助您找回宝贵的回忆。 图片来…

Laravel03 路由到控制器与连接数据库

Laravel03 路由到控制器与连接数据库 1. 路由到控制器2. 连接数据库 1. 路由到控制器 如下图一些简单的逻辑处理可以放在web.php中&#xff0c;也就是路由的闭包函数里面。但是大的项目&#xff0c;我们肯定不能这么写。 为什么保证业务清晰好管理&#xff0c;都应该吧业务逻辑…

【Linux深入剖析】进程优先级 | 命令行参数 | 环境变量

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.进程优先级2.Linux…

[C++]使用C++实现监控文件是否被修改

软件开发过程中经常会用到配置文件,某些应用场景要求在软件运行时动态修改配置文件,此时就需要监控配置文件是否被修改,下面我们就来看看如何使用C实现这一功能吧 软件开发过程中经常会用到配置文件&#xff0c;某些应用场景要求在软件运行时动态修改配置文件&#xff0c;此时…

国产服务器操作系统

为何记录 最近的开发工作经常接触到国产服务器操作系统的业务&#xff0c;经常被x86、arm、龙芯、鲲鹏、欧拉...搞得一脸懵逼&#xff0c;遂记之&#xff01; 操作系统 这里按照应用场景分&#xff1a; 桌面操作系统&#xff1a;主要用于pc&#xff0c;如Windows、macOS、Li…

MES系统生产订单管理:多角度全面解析

一、MES系统生产订单管理概述 MES中的生产订单管理涉及到订单的接收、处理、执行和跟踪等多个方面。MES系统生产订单管理旨在确保生产过程中的订单信息准确无误、生产进度可控&#xff0c;从而实现高效、有序的生产。 二、生产订单管理的核心功能 订单接收与处理&#xff1a;…

30-k8s集群的七层代理-ingress资源(进阶知识)

一、ingress概述 1&#xff0c;引发问题 目前使用svc资源做网络暴露&#xff0c;使用nodeport类型&#xff0c;一个业务对应一个宿主机端口&#xff0c;那么如果业务多了&#xff0c;所占用的宿主机端口也就多了&#xff0c;虽然说宿主机端口一般情况下都是够用的&#xff0c;…

Android Jni的介绍和简单Demo实现

Android Jni的介绍和简单Demo实现 文章目录 Android Jni的介绍和简单Demo实现一、JNI的简单介绍JNINDKJni的开发背景&#xff1a;**JNI在 Android 开发里的主要应用场景&#xff1a;** 二、JNI的简单Demo1、Demo主要界面和效果展示2、CMake编译加载文件add_library 指令的加载库…

主从复制实现Redis集群

主从复制实现Redis集群实验 (一主二从): 实验环境: 使用Docker 搭建 Redis 版本 5.0.5 打开一个终端窗口&#xff0c;在其中运行如下命令创建一个名为redis-master的Redis容器。注意&#xff0c;它的端口是6379 (本地的端口:映射到容器的端口) docker run -itd--name redis-m…

深入理解 JavaScript 对象原型,解密原型链之谜(下)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

VScode连接远端服务器一直输入密码解决方法

文章目录 1 关闭远程连接2打开命令面板3 输入remote-ssh: kill vs code server on host… 1 关闭远程连接 2打开命令面板 3 输入remote-ssh: kill vs code server on host… remote-ssh: kill vs code server on host… 然后一路回车(选中出问题的主机)&#xff0c;输一遍密码…

缓存一致性问题的解决策略

缓存一致性问题的背景和概念介绍 在一个系统中&#xff0c;我们通常使用数据库来存储数据&#xff0c;以保证数据的持久性。但是&#xff0c;由于数据库的读写速度相对较慢&#xff0c;如果每次请求都直接访问数据库&#xff0c;会降低系统的响应速度。为了提高系统的性能&…

《TCP/IP详解 卷一》第7章 防火墙和NAT

7.1 引言 NAT通常改变源IP和源端口&#xff0c;不改变目的IP和目的端口。 7.2 防火墙 常用防火墙&#xff1a; 包过滤防火墙&#xff08;packet-filter firewall&#xff09; 代理防火墙&#xff08;proxy firewall&#xff09; 代理防火墙作用&#xff1a; 1. 通过代理服务…