Eloquent JavaScript #10# Modules

Notes

一、背景问题npm

理想的程序:相似于乐高玩具。它具备清晰的结构,工做方式很容易解释,每一个部分都扮演着明确的角色。编程

现实的程序:有机地增加。随着新需求的出现,新功能被添加。结构化和维持结构化是服务于将来的工做,所以很容易忽略它并逐渐让程序的各个部分变得很是纠结。数组

形成的问题:首先,理解这样的一个系统很困难。其次,没法对系统的某部分功能重用,与其把该功能从上下文提取出来,不如重写。简而言之就是高度耦合。数据结构

 

二、模块Modulesapp

目的:解决高度耦合的问题ide

模块的定义:a piece of program,指明了自身所依赖的程序,以及它向外部所提供的功能(interface),其他部分保密。模块化

模块间的关系:依赖(dependencies)。当模块的依赖被定义在它自身时,就可使用它来肯定须要存在哪些其余模块才能使用给定模块并自动加载依赖项。函数

如何实现模块化:首先须要程序员有这个意识,其次须要实际编程上的一些辅助措施。

 

三、软件包Packages

定义:一大块能够发布(复制和安装)的代码。它可能包含一个或多个模块,而且包含有关其所依赖的其余软件包的信息。一个软件包一般还附带文档来解释它的功能,以便那些没有编写它的人仍然可使用它。

针对的问题:咱们能够经过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指明所需的依赖以及控制依赖的加载。

 

五、Evaluating data as code

若是咱们但愿依赖关系成为代码的一部分(能够类比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

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));
/**
 * 一、检查"plusOne"是不是require.cache的一个属性
 * 二、若是是,直接返回require.cache["plusOne"].exports
 * 三、若是不是,经过readFile读取plusOne模块的代码
 * 四、require.cache["plusOne"]绑定module(含有一个空对象的exports)
 * 五、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);

 

七、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);

 

八、Building and bundling

不少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. ”,并且模块的功能越是单1、通用,越是易于和其它模块组合。
  • 有时定义状态对象是有用的,但若是函数足够,就用一个函数。“您​​首先建立一个对象,而后将该文件加载到您的对象中,最后使用专门的方法来得到结果。这种东西在面向对象的传统中很常见,并且很糟糕。您不能调用单个函数并继续前进,而是必须执行将对象移动到各类状态的仪式。由于数据被包装在一个专门的对象类型中,因此与它交互的代码必须知道该类型,从而产生没必要要的相互依赖性。”
  • 一般没法避免定义新的数据结构,可是当一个数组足够时,使用一个数组。

 

Exercises

① A modular robot

我会怎么作:把各个函数变得更加通用、独立。。

------- --------  ———— -- —— ——- --  -- - -- - -

② Roads module

// 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("-")));

------- --------  ———— -- —— ——- --  -- - -- - -

③ Circular dependencies

暂略。

相关文章
相关标签/搜索