Notes
理想的程序:类似于乐高玩具。它具有清晰的结构,工作方式很容易解释,每个部分都扮演着明确的角色。
现实的程序:有机地增长。随着新需求的出现,新功能被添加。结构化和维持结构化是服务于未来的工作,因此很容易忽略它并逐渐让程序的各个部分变得非常纠结。
造成的问题:首先,理解这样的一个系统很困难。其次,无法对系统的某部分功能重用,与其把该功能从上下文提取出来,不如重写。简而言之就是高度耦合。
目的:解决高度耦合的问题
模块的定义:a piece of program,指明了自身所依赖的程序,以及它向外部所提供的功能(interface),其余部分保密。
模块间的关系:依赖(dependencies)。当模块的依赖被定义在它自身时,就可以使用它来确定需要存在哪些其他模块才能使用给定模块并自动加载依赖项。
如何实现模块化:首先需要程序员有这个意识,其次需要实际编程上的一些辅助措施。
定义:一大块可以发布(复制和安装)的代码。它可能包含一个或多个模块,并且包含有关其所依赖的其他软件包的信息。一个软件包通常还附带文档来解释它的功能,以便那些没有编写它的人仍然可以使用它。
针对的问题:我们可以通过Copy代码来复用一些函数、功能,但是当这些函数、功能更新,就不得不在每一处修改它。如果在程序包中发现问题或添加了新功能,则依赖它的程序(也可能是包)只需要更新程序包就可以了。
基础设施:以这种方式工作需要基础设施。我们需要一个存储和查找包的地方以及安装和升级它们的便捷方式。在JavaScript世界中,此基础结构由NPM(https://npmjs.org)提供。
把js代码放在不同文件中不能满足需求,不同的文件同样共享相同的全局命名空间。它们之间会相互影响,并且代码的依赖结构不够清晰。
直到2015年js都没有内建的模块系统。所以最初的模块系统都是自己设计的:
const weekDay = function() { const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name(number) { return names[number]; }, number(name) { return names.indexOf(name); } }; }(); console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday
如上,该模块的接口包括weekDay.name和weekDay.number,局部绑定被隐藏在函数作用域里。
这种方式提供了一定程度的独立性,但它只指明了向外部提供的功能(interface),并没有指明自身需要的依赖,只是期望于外部环境能够提供这些依赖,更别说什么自动加载了。(当然这里的例子并不需要其它依赖)
很长一段时间,这是Web编程中使用的主要方法,但现在它已经过时了。
理想的模块系统应该类似于Java里的包系统,可以通过import指明所需的依赖以及控制依赖的加载。
如果我们希望依赖关系成为代码的一部分(可以类比Java的import),就必须用代码来控制依赖的加载。做到这一点需要能够把字符串(或者说数据)作为代码执行【???】
首先是一种不推荐方式——特殊运算符eval,它容易破坏scope中原有的一种属性(就是缺乏封闭性):
const x = 1; function evalAndReturnX(code) { eval(code); return x; } console.log(evalAndReturnX("var x = 2")); // → 2 console.log(x); // → 1
采用Function的构造器是一种风险较低的方法,它把代码包装在函数值里,这样它就有自己的作用域(scope)而不会影响别的作用域了:
let plusOne = Function("n", "return n + 1;"); console.log(plusOne(4)); // → 5
这正是我们的模块系统所需要的,我们可以把模块代码包装在一个函数里,函数的作用域即是模块的作用域。
CommonJS modules是最常用的用于规范化js模块的模块。
CommonJS modules的核心概念是一个叫require的函数,当你传入一个模块名并调用这个函数时,它确保模块已经被加载并返回该模块提供的接口。
下面是一个模块实现示例(直接脑补成java里的import package就很好懂了):
const ordinal = require("ordinal"); // 模块的依赖 const {days, months} = require("date-names"); // 模块的依赖 exports.formatDate = function(date, format) { // 模块对外提供的接口,可以是一个函数,也可以是像date-names一样多个函数 return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => { if (tag == "YYYY") return date.getFullYear(); if (tag == "M") return date.getMonth(); if (tag == "MMMM") return months[date.getMonth()]; if (tag == "D") return date.getDate(); if (tag == "Do") return ordinal(date.getDate()); if (tag == "dddd") return days[date.getDay()]; }); };
模块把它的接口函数绑定到exports上,以便其它模块可以访问它:
const {formatDate} = require("./format-date"); // 访问刚定义的formatDate模块 console.log(formatDate(new Date(2017, 9, 13), "dddd the Do")); // → Friday the 13th
我们可以自定义一个轻量级的require:
require.cache = Object.create(null); function require(name) { if(!(name in require.cache)) { // 防止重复加载 let code = readFile(name); // readFile并非标准函数,需要自定义 let module = { exports: {} }; require.cache[name] = module; let wrapper = Function("require, exports, module", code); // 加载代码 wrapper(require, module.exports, module); // 这样模块接口就被绑定到module.exports了 } return require.cache[name].exports; // => module.exports }
举个例子(模拟):
const codeOfPlusOne = "exports.plusOne = n => n + 1;"; const readFile = (name) => { if (name == "plusOne") return codeOfPlusOne; } require.cache = Object.create(null); function require(name) { if(!(name in require.cache)) { // 防止重复加载 let code = readFile(name); // readFile并非标准函数,需要自定义 let module = { exports: {} }; require.cache[name] = module; let wrapper = Function("require, exports, module", code); // 加载代码 wrapper(require, module.exports, module); // 这样模块接口就被绑定到module.exports了 } return require.cache[name].exports; // => module.exports } const {plusOne} = require("plusOne"); console.log(plusOne(5)); /** * 1、检查"plusOne"是否是require.cache的一个属性 * 2、如果是,直接返回require.cache["plusOne"].exports * 3、如果不是,通过readFile读取plusOne模块的代码 * 4、require.cache["plusOne"]绑定module(含有一个空对象的exports) * 5、wrapper实际变成下面那样:加载-> 函数绑定到require.cache["plusOne"] */
对wrapper的分析:
// 1.构造wrapper,加载代码 let wrapper = (require, exports, module) => { // 模块内容↓ // const ordinal = require("ordinal"); exports.plusOne = n => n + 1; } // 2.调用wrapper,实际绑定 wrapper(require, module.exports, module); // exports.plusOne = f(x); // => ... module.exports = f(x);
7、ECMAScript modules(since 2015)
虽然CommonJS modules已经足够好用,但还是有那么一点瑕疵,例如:你添加到exports的东西在局部作用域居然不可用。
这就是为什么js要推出自己的模块系统:
import ordinal from "ordinal"; import {days, months} from "date-names"; export function formatDate(date, format) { /* ... */ }
主要概念保持不变,但是细节有些不同。符号现在已整合到语言中。您可以使用特殊import
关键字,而不是调用函数来访问依赖项。
export的不再是函数,而是一系列的绑定。
把模块import到没有{ }包围的绑定时,返回模块的default绑定(需要自定义):
export default ["Winter", "Spring", "Summer", "Autumn"];
还可以对模块进行重命名:
import {days as dayNames} from "date-names";
console.log(dayNames.length);
很多js代码其实不是用js写的,而是其它语言编译过来的。
因为单个文件传输比较快,因此程序员通常会在发布代码前用一种被叫做bundlers的工具把n个js文件压缩成一个js文件。
除了文件数量,文件大小同样影响传输速率,可以通过叫minifiers的工具去除空格和注释。
总而言之: Just be aware that the JavaScript code you run is often not the code as it was written.
- 良好的程序设计是主观的 - 涉及权衡和品味问题。
- 模块设计的一个方面是易于使用,这可能意味着遵循现有的惯例,模仿标准功能或广泛使用的软件包是一个好主意。
- 保持模块功能单一。“Even if there’s no standard function or widely used package to imitate, you can keep your modules predictable by using simple data structures and doing a single, focused thing. ”,而且模块的功能越是单一、通用,越是易于和其它模块组合。
- 有时定义状态对象是有用的,但如果函数足够,就用一个函数。“您首先创建一个对象,然后将该文件加载到您的对象中,最后使用专门的方法来获得结果。这种东西在面向对象的传统中很常见,而且很糟糕。您不能调用单个函数并继续前进,而是必须执行将对象移动到各种状态的仪式。因为数据被包装在一个专门的对象类型中,所以与它交互的代码必须知道该类型,从而产生不必要的相互依赖性。”
- 通常无法避免定义新的数据结构,但是当一个数组足够时,使用一个数组。
Exercises
我会怎么做:把各个函数变得更加通用、独立。。
------- -------- ———— -- —— ——- -- -- - -- - -
// Add dependencies and exports const {buildGraph} = require("./graph"); const roads = [ "Alice's House-Bob's House", "Alice's House-Cabin", "Alice's House-Post Office", "Bob's House-Town Hall", "Daria's House-Ernie's House", "Daria's House-Town Hall", "Ernie's House-Grete's House", "Grete's House-Farm", "Grete's House-Shop", "Marketplace-Farm", "Marketplace-Post Office", "Marketplace-Shop", "Marketplace-Town Hall", "Shop-Town Hall" ]; exports.roadGraph = buildGraph(roads.map(r => r.split("-")));
------- -------- ———— -- —— ——- -- -- - -- - -
暂略。