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,而无需把所有的模块都引入页面。

<body>
    <script src="js/require.js" type="text/javascript" charset="utf-8" data-main="js/a.js"></script>
    <script type="text/javascript">
        console.log('page loaded..');
    </script>
</body>

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. 执行结果

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 的过程中,我们可以并不指定任何其他模块的显式导入,而是在需要使用时再手动调用:

<script src="js/sea.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
    
    // 通过 seajs 对象调用 use() 方法获取其他模块的引用
    seajs.use('./js/aa.js', function (aa) {
        aa.printBB();
        aa.printCC();
    });
    
    // 未阻塞的页面其他 js 语句
    console.log('page loaded...');
            
</script>

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. 执行结果

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容