一周一章前端书·第5周:《你不知道的JavaScript(上)》S01E05

第5章:作用域闭包

到底什么是闭包

  • 本章讲解闭包(Closures),它与作用域工作原理息息相关。
  • 首先我用自己总结的三句话,简单说明什么是闭包:
    • (1)首先我们要知道,变量的查找 规则 是由内到外的
    • (2)所以 子函数可以访问外部作用域 的变量;
    • (3)如果 把子函数赋值给外部变量 时,此时外部变量就 拥有 了可以 访问封闭数据包的能力
  • 一个简单的闭包示例
function foo(){
    var a = 2;
    function bar(){
        console.log(a); //2
    }
    return bar;
}

var baz = foo();
  • 分析上述代码:
    • bar函数能访问外部foo函数的作用域,将bar传递给外部变量baz来执行;
    • 此时bar函数在原来定义的词法作用域之外执行,同时持有foo函数作用域的引用,这就叫作闭包。
  • 并且通过闭包的执行方式,foo函数在执行后,其作用域不会被立即销毁(毕竟bar函数还要用的啊)

闭包暴露的方式不止一种

  • 闭包函数除了可以直接赋值给外部变量,也可以通过执行外部函数,将闭包函数以参数传递的方式暴露出去。
var foo(){
    var a = 2;
    //闭包函数
    function baz(){
        console.log(a);; //2
    }
    //执行外部函数,将闭包函数通过参数的方式传递进去
    bar(baz)
}

var bar(fn){
    fn();
}

闭包是最熟悉的陌生人

  • 虽然闭包比较隐晦,但它绝不仅仅是一个好玩的玩具而已,在我们的代码中到处都有它的身影。
  • 比如常见的定时函数setTimeout()
function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000)
}

上述代码等价于:

//全局的setTimout函数准备就绪
var setTimout = function(invokeFn){}
function wait(message){
    //timer内部函数拥有对mesage的访问权
    var timer = function(){
        console.log(message);
    }
    //执行setTimeout()函数,并将timer以参数方式传递进去
    setTimout(timer);
}
  • 是不是有似曾相似的感觉?内部函数timer()具有外部函数wait()作用域中的message变量的引用,它就是闭包。
  • 除了定时函数之外,jQuery代码也普遍的在使用闭包,不信你看下面的代码:
//参数传递一个name字符串,选择器字符串
function setupBot(name,selector){
    //通过选择器字符串初始化成jQuery对象
    //绑定点击事件
    $(selector).click(function activator(){
        //打印外部函数的name
        console.log('Activating:' + name);
    })
}

setupBot('Closure Bot 1','#bot_1');
setupBot('Closure Bot 2','#bot_2');
  • 其实无论何时何地,只要将函数当做参数进行传递,就有闭包的应用。
  • 什么定时器、事件监听函数、Ajax请求回调函数、跨窗口通信、Web Workers等代码中,都普遍应用到了闭包。
  • 它每天与我们擦肩而过,就好像那个最熟悉的陌生人。

闭包解决了什么问题

1. 用闭包造块级作用域

  • 我们先看问题: 我只是想依次输出循环的i(1 ~ 5)
for(var i=1;i<=5;i++){
    setTimtou(function timer(){
        console.log(i);  
    },0)
}
  • 见鬼了,然而输出的结果却是五次6,这是为什么呢?
  • 其根本原因是,定时器的回调函数永远在循环结束后才执行。
  • 那你可能会想,哦,那我岂不是永远都不能在for循环中用定时器了?JavaScript真垃圾!诶诶,先别慌,我们来分析一下。
  • 我们不是预期循环的每个迭代中,都有一个i的副本,然后输出它吗?通过闭包就能实现。
for(var i=1;i<=5;i++){
    (function(index){
        setTimtou(function timer(){
            console.log(index);  
        },0)
    })(i);
}
  • 我们通过IIFE构造了一个块级作用域将i存了起来。
  • 提到块级作用域,其实ES6的语法里还有一种更便捷的解决方式——let声明
for(let i=1;i<=5;i++){
    setTimtou(function timer(){
        console.log(i);  
    },0)
}

2. 用闭包造模块

function CoolModule(){
    var something = 'cool';
    var another = [1, 2, 3];
    
    function doSomething(){
        console.log(something);
    }
    
    function doAnother(){
        console.log(another.join(","));
    }
    
    //对外暴露内部函数
    return {
        doSomething : doSomething,
        doAnother : doAnother
    }
}
  • 上述代码演示了JavaScript模块暴露。通过调用CoolModule函数创建一个模块实例,CoolModule返回的对象中包含内部函数的引用,就相当于模块的公共API。
  • 当然上述代码可以任意调用多次,重复返回新的模块实例,我们可以改成单例模式:
//通过一个IIFE函数来包装
var foo = (function CoolModule(){
    var something = 'cool';
    var another = [1, 2, 3];
    
    function doSomething(){
        console.log(something);
    }
    
    function doAnother(){
        console.log(another.join(","));
    }
    
    //对外暴露内部函数
    return {
        doSomething : doSomething,
        doAnother : doAnother
    }
})();

foo.doSomething();  //cool
foo.doAnother();    //1,2,3
  • 模块的公共API不仅可以是内部私有变量的访问,也可以是修改私有变量的方法:
var foo = (function CoolModule(id){
    var moduleId = id;

    function showId(){
        console.log(moduleId);
    }

    function uppcaseId(){
        moduleId = moduleId.toUpperCase();
    }

    return{
        showId : showId,
        uppcaseId : uppcaseId
    }
})('fooModule');

foo.showId();
foo.uppcaseId();
foo.showId();
  • 其实大多数模块管理机制本质是也是通过类似的方式来实现的,我们来尝试写一个简版的模块管理器:
/**
 * 定义牛批哄哄的超级模块管理器
 */
var SuperModules = (function(){
    //所有的模块集合,以name作为key
    var moduleMap = {};
    
    function define(name,deps,impl){
        //获取依赖
        for(var i=0;i<deps.length;i++){
            deps[i] = moduleMap[deps[i]]        
        }
        //执行引入的模块,并以deps作为参数
        moduleMap[name] = impl.apply(impl,deps);
    }
    
    function get(name){
        reutrn moduleMap[name];
    }
    
    //暴露公共API
    return{
        define : define,
        get : get
    }
})()

/**
 * 先定义一个bar模块
 * 没有依赖,impl是一个执行函数
 */
SuperModules.define('bar',[],function(){
    function hello(name){
        return 'let me introduce:' + name;
    }
    return {
        hello : hello
    }
})

/**
 * 再定义一个foo模块
 */
SuperModules.difine('foo',['bar'],function(bar){
    var hungry = 'hippo';
    
    function awesome(){
        console.log(bar.hello(hungry).toUpperCase());
    }
    
    return {
        awesome : awesome
    }
})

/**
 * 调用测试
 */
var bar = SuperModules.get('bar');
var foo = SuperModules.get('foo');

//let me introduce: hippo
console.log(bar.hello('hippo'));
//let me introduce: HIPPO
foo.awesome();
  • 是不是看起来比较复杂……不过不用担心,ES6以及添加了模块的语法支持!
  • ES6会将每个js文件当做独立的模块来处理,每个模块可以通过import关键字导入依赖的模块,或者通过export关键字导出API。你需要做的,只是拥抱ES6!
  • bar.js
function hello(name){
    return 'let me introduce:' + name;
}
export hello;
  • foo.js
import hello from 'bar';

var hungry = 'hippo';
function awesome(){
    console.log(bar.hello(hungry).toUpperCase());
}
export awesome;
  • baz.js
module foo from 'foo';
module bar from 'bar';

//let me introduce: hippo
console.log(bar.hello('hippo'));
//let me introduce: HIPPO
foo.awesome();

小结

  • 内部函数可以访问外部函数的作用域,如果将内部函数暴露给外部变量时,或者说内部函数在定义的词法作用域之外执行时,就产生了闭包。
  • 闭包是个非常强大的工具,可以用来实现模块模式。
  • 模块的特征:
    • 为创建内部作用域而调用一个包装函数;
    • 包装函数的返回值必须至少包含一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容