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

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

前言

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

JavaScript ??榛硐?/h2>

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

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

AMD 规范

? ? ? ?AMD 即 Asynchronous Module Definition,中文名是“异步模块定义”的意思。它是一个在浏览器端??榛⒌墓娣?。??榻灰觳郊釉?,??榧釉夭挥跋旌竺嬗锞涞脑诵?。所有依赖某些??榈挠锞渚胖迷诨氐骱?。典型的实现是 requirejs

requirejs 中通过 define(id?, dependencies?, factory) 函数定义??椋问硎镜暮迨牵?br> id [可选]:定义中??榈拿帧H绻挥刑峁└貌问?,模块的名字应该默认为??榧釉仄髑肭蟮闹付ń疟镜拿帧H绻峁┝烁貌问?,??槊谟τ没肪持胁辉市碇馗础?br> 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. ??槎ㄒ?/h4>
// 文件: 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 规范相同,只不过对于??榈亩ㄒ宸绞胶图釉厥被杂胁煌4聿酚?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 规范最大的不同,便是对于??榈募釉厥被?code>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 ??楣娣?/strong> 是官方拟定的一套??榧釉毓娣叮淠?橐梅绞胶陀锓ǘ加肭罢呗杂胁煌?/p>

??榈汲?/h4>

? ? ? ?ES6 通过 export defaultexport 两种语法进行??榈汲觯汲龅慕峁且桓?strong>??槎韵?/strong>,而两种语法会分别将导出结果作为这个模块对象的属性。他们之间的差别是:

export default:导出的值作为模块对象default 属性,可由导入方进行重命名。一个??橹兄辉市沓鱿忠桓?export default。
export:导出的语法必须是一个声明语句,声明的标识符(变量名或函数名)会作为导出的??槎韵?/strong>的属性进行动态绑定。一个??橹锌梢猿鱿侄喔?export 语法,并且可以与 export default 语法共存。

示例:

// 模块 foo
export const num1 = 10;

export const num2 = 19;

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

??榈既?/h4>

接收默认导入:

// 模块 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阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容