JS 函数式编程思维简述(七):闭包 04

  1. 简述
  2. 无副作用(No Side Effects)
  3. 高阶函数(High-Order Function)
  4. 柯里化(Currying)
  5. 闭包(Closure)
    -- JavaScript 作用域
    -- 面向对象关系
    -- this调用规则
    -- 配置多样化的构造重载
    -- 更多对象关系维护——模块化
    -- 流行的模块化方案
  6. 不可变(Immutable)
  7. 惰性计算(Lazy Evaluation)
  8. Monad

前言

       在一个封闭的执行空间中(如函数),调用执行时会在内存中创建其执行上下文。而在上下文执行完毕时,本应当销毁的执行结果并没有销毁,保留了下来由调用者继续引用。这就是闭包(Closure)
       闭包的表现形式可以是多样的,最稀松常见的便是返回一个对象,由调用主函数获取该对象这种方式。当然,也可以是非常复杂的过程构建,比如构建一个模块化的交互环境。

JavaScript 模块化表现

       早些年间,JavaScript 并不是很受人待见。所有的执行环境都在一个页面中混为一体,每一个 jser 都需要考虑自己写的外部 .js 文件如何能够不被其他的插件所干扰。很多相似的代码片段重复出现,大量的造轮子,搞的一个页面环境十分笨重。让人觉得 JavaScript 只能用来玩一玩,根本无谈工程化,上不了台面。
       终于,前辈们不堪其扰,决心杀出一条血路,为构建更优质的 js 环境而不懈努力。他们要解决的问题是:

  • 命名冲突:相似意义的变量或函数的命名冲突,可能会导致全局其他地方的引用产生歧义;
  • 功能解耦:一个功能强大的 js 插件往往包含诸多内容,致使插件设计者在维护时不堪重负。需要将一个复杂的问题解耦成若干简单的问题,并且互相协作引用。
  • 功能依赖:简单的诸多小的功能点可以被多处重复引用依赖,最终形成复杂的应用。因此需要配备成熟的依赖方式。
  • 工程维护:在设计上解构了一个复杂的应用,在人员配备方面便可以做到分工明确。由独立的人或团队负责某一个或一部分模块的设计,在接口引用的过程中只要符合标准,则开发效率也会大大提升,利于进一步维护。

AMD 规范

       AMD 即 Asynchronous Module Definition,中文名是“异步模块定义”的意思。它是一个在浏览器端模块化开发的规范。模块将被异步加载,模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。典型的实现是 requirejs。

requirejs 中通过 define(id?, dependencies?, factory) 函数定义模块,参数表示的含义是:
id [可选]:定义中模块的名字。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名在应用环境中不允许重复。
dependencies [可选]:当前模块依赖的其他模块标识(模块名)所组成的数组字面量。如果忽略此参数,则默认为["require", "exports", "module"]。
factory : 模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

require.js 示例

       点击下载 require.js

1. 目录结构

// 当前示例的目录结构
├─ js
│  ├─ require.js
│  ├─ a.js
│  ├─ b.js
│  └─ c.js
├─ index.html

2. 页面引用

       在引用 require.js 时,我们标注了其他模块的加载主入口是 a.js,而无需把所有的模块都引入页面。


    
    

3. 模块定义

// 文件: a.js
// 假设模块 a 依赖于模块 c 和 b
define('a', ['c', 'b'], function(c, b){
    
    // 模块 a 的执行环境
    console.log('a被加载...');
    
    c.printC('a');
    b.printB('a');

    console.log('a加载完毕...');
    
});

之后是模块 c 和 b 的定义

// 文件: c.js
define('c', function(){
    console.log('c被加载...');
    
    // 定义函数 printC
    const printC = function(who){
        console.log(who + ' print C!!!');
    }
    // 导出调用对象
    return {
        printC
    };
});
// 文件: b.js
define('b', function(){
    console.log('b被加载...');

    // 定义函数 printC
    const printB = function(who){
        console.log(who + ' print B!!!');
    }
    // 导出调用对象
    return {
        printB
    };
});

4. 执行结果

JS 函数式编程思维简述(七):闭包 04_第1张图片
image

       requirejs 以异步的方式进行模块定义,并未阻塞程序主线程。因此其他的 js 活动可以正常运行。而在加载模块依赖的过程中,则会根据声明的依赖标识逐一加载。此时,并无关联性的三个 js 文件就可以互相引用,完成了功能解构。

CMD 规范

       CMD 即 Common Module Definition,通用模块定义,是国内发展起来的一套 js 模块化规范,所解决的问题与 AMD 规范相同,只不过对于模块的定义方式和加载时机略有不同。代表产品有 Alibaba 的玉伯所设计的 Sea.js。与 AMD 规范不同的是 AMD 规范推崇依赖加载前置,而 CMD 规范推崇依赖就近。从依赖调用过程中我们就能看出他们之间的差别。

sea.js 示例

       点击下载 sea.js

1. 目录结构

// 当前示例的目录结构
├─ js
│  ├─ sea.js
│  ├─ aa.js
│  ├─ bb.js
│  └─ cc.js
├─ index.html

2. 页面引用

       使用 sea.js 的过程中,我们可以并不指定任何其他模块的显式导入,而是在需要使用时再手动调用:



3. 模块定义

// aa.js
define(function (require, exports, module) {
    
    console.log('aa被加载...');
    
    // 在需要时引用的其他模块,而非定义时描述模块
    const cc = require('./cc');
    const bb = require('./bb');
    
    // 通过 module.exports 导出模块
    module.exports = {
        printCC: cc.print,
        printBB: bb.print
    };
});

其他的依赖模块 bb.jscc.js

// bb.js
define(function (require, exports, module) {
    
    console.log('bb被加载...');
    
    // 导出模块内容
    module.exports = {
        print: function(){
            console.log('bb 执行 print()!');
        }
    };
});
// cc.js
define(function (require, exports, module) {
    
    console.log('cc被加载...');
    
    // 导出模块内容
    module.exports = {
        print: function(){
            console.log('cc 执行 print()!');
        }
    };
});

4. 执行结果

JS 函数式编程思维简述(七):闭包 04_第2张图片
image

       与 AMD 规范最大的不同,便是对于模块的加载时机:aa 首先被加载,在 aa 模块执行的过程中按需加载了 ccbb 模块。
       不过值得一提的是,随着 ES6 中对模块化的定义以及普及,民间的优秀规范也渐渐被弃用。Sea.js 上一次的更新时间是 2014 年,已停止维护。但他们都是非常优秀的闭包案例,值得学习。

CommonJS 规范

       CommonJS 规范node.js 中的模块化规范,其应用方式与 CMD 规范十分相似,但要简化一些。CommonJS 规范 的实现方式是:

(function(exports, require, module, __filename, __dirname){
  return module.exports;
});

熟悉 node.js 环境的同学能看到在模块参数中,node为我们传递了全局变量 __filename__dirname 以便于更方便的引用。而我们自己在写基于 node.js 环境的 js 源码时,便可以很方便的直接这样定义:

let fs = require('fs');

// 定义一个读取文件返回 Promise 对象的异步函数
let readFile = (txtOrig) => new Promise((resolve, reject) => {
    fs.readFile(txtOrig, {encoding: 'utf8'}, (err, data) => {
        if(err) reject(err);
        resolve(data);
    });
});

module.exports = {
    readFile
}

模块定义的语法被隐去了,留给开发者的是可以更加关注业务流程,减少了代码冗余。

ES6 模块规范

       ES6 模块规范 是官方拟定的一套模块加载规范,其模块引用方式和语法都与前者略有不同。

模块导出

       ES6 通过 export defaultexport 两种语法进行模块导出,导出的结果是一个模块对象,而两种语法会分别将导出结果作为这个模块对象的属性。他们之间的差别是:

export default:导出的值作为模块对象default 属性,可由导入方进行重命名。一个模块中只允许出现一个 export default
export:导出的语法必须是一个声明语句,声明的标识符(变量名或函数名)会作为导出的模块对象的属性进行动态绑定。一个模块中可以出现多个 export 语法,并且可以与 export default 语法共存。

示例:

// 模块 foo
export const num1 = 10;

export const num2 = 19;

export default function(a, b){
    return a + b;
}

模块导入

接收默认导入:

// 模块 bar
// 默认导入时可以为导入的结果重新命名
import calc from './foo';
calc(2, 3); // 结果: 5

接收声明导入:

// 模块 bar
// 通过 export 导出的声明,必须以原命名进行导入
import {num1, num2} from './foo';
num1; // 结果: 10
num2; // 结果: 19

全部导入:

// 模块 bar
import calc, {num1, num2} from './foo';
calc(num1, num2); // 结果: 29

通配符导入:

// 模块 bar
import calc, * as Num from './foo';
calc(Num.num1, Num.num2); // 结果: 29

总结:闭包是一个保护执行环境、缓存执行结果的设计方式。在各种支持函数式编程的语言中我们都能看到闭包的身影,它的概念很模糊,却无处不在。

你可能感兴趣的:(JS 函数式编程思维简述(七):闭包 04)