拒绝抄书,彻底消化闭包

前言

之前写过关于闭包的文章,本来以为自己懂了,后来面试时被问到怀疑人生。才明白自己只是觉得自己明白了而已,如果说要将一个东西理解的彻彻底底,就不能“抄书”(我之前就是抄书),而是死抠每一个知识点,一点含糊都会让整个系统崩塌。原文地址

ok,现在开始死抠。什么是闭包?

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁 ——来自于百度百科

闭包是基于词法作用域书写代码时所产生的自然结果。当函数可以记住并访问所在的词法作用域时,就产生了闭包。 ————《你不知道的js(上)》

看不太懂,那就拆开看,什么是词法作用域?

词法作用域

如图,每个框框中都是一个作用域,引擎在执行console.log()时(黄色框中的语句),会从内向外逐个作用域查找变量。在baz中,我们找到了变量c,没有找到a,b,就会往上一层找,bar中有b,c,baz,找到了b,同名变量c被忽略,以此类推,直至所有执行语句都匹配了变量,否则引擎解析失败抛出错误。

图示

除了词法作用域,还有啥?

其实作用域包括词法作用域和动态作用域,JavaScript中的作用域是词法作用域(大部分的编程语言也是基于词法作用域)。在上面的图中,我们能清晰地看出来,每个函数的全部变量都可以在整个函数的范围中使用或复用(嵌套的函数可以使用外部函数的变量),这就是函数作用域。那么只有函数才能创建作用域“框框”吗?

我们看下面这几句代码:

for(var b=0;b<3;b++){}

console.log('b',b) // 3

上面的代码中,没有声明任何函数,所以通过var声明的变量b被绑定到外部作用域上,也就是全局。(不了解变量提升的同学,可以看我的这篇文章=>《详解ES6暂存死区TDZ》),所以上述代码相当于:

var b;
for(b=0;b<3;b++){}
console.log('b',b) // 3

。。。是不是很奇葩,本来只想让变量b在for循环中使用,for循环之后销毁,为啥要让他污染到整个词法作用域嘞?幸运的是,由于人类的探索精神,和几个浏览器爹们对JavaScript这个不健全的儿子的扶持,ES6中有了let和const,作为块作用域的补充。(明明都9012了,我为啥还在写ES6的东西=.=)如下,b在for循环结束时就会被销毁,又由于词法作用域中不存在同名变量,所以这里会报错。

for(let b=0;b<3;b++){}
console.log('b',b) // Uncaught ReferenceError: b is not defined

我们在理解块作用域的时候,可以将一个{}中看成一个块。

作用域和上下文到底是不是一个东西?

答案肯定是"NO!!"上文中我们已经明白了,作用域是在函数定义时决定的。上下文其实就是函数中this的指向,即当前函数运行时所挂载的对象。

const a=1
function foo(){
    console.log(this.a)
}

const obj={a:2,foo}

foo() // undefined
obj.foo() // 2

这里有个小tips,为啥const声明的a,没有像var一样挂载到window上呢?其实秘密在这里,《Javascript闭包:从理论到实现,[[Scopes]]的每一根毛都看得清清楚楚》 (写本章时我也没仔细读这篇文章),const 声明的a其实是在[[scopes]]上。

循环和闭包

一道经典面试题

以下代码为什么与预想的输出不符?

// 代码块1
for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 输出5次5
    }, 0)
}

假设A:因为setTimeout这块的任务直接进入了事件队列中,所以i循环之后i先变成了5,再执行setTimeoutsetTimeout中的箭头函数会保存对i的引用,所以会打印5个5.

变体一:

// 代码块2
for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 输出 0,1,2,3,4
    }, 0)
}

假设结论A成立,那么上式应该也是输出5次5,但是很明显不是,所以结论A并不完全正确。

那我们去掉循环,先写成最简单的异步代码:

function test(a){
    setTimeout(function timer(){
        console.log(a)
    })
}

test('hello')

执行testsetTimeouttimer函数放入了事件队列,timer保留着test函数的作用域(在函数定义时创建的),test执行完毕,主线程上没有其他任务了,timer从事件队列中出队,执行timer,执行console.log(a),由于闭包的原因,a依然会保留着之前的引用,输出'hello'

那我们在回到题目中,因为两段代码中的不同只有声明语句,所以我们提出假设B:因为在代码块1中,匿名函数保留着外部词法作用域,i都是在全局作用域上,代码块2中由于存在块作用域,所以它保留着每次循环时i的引用。

变体二:

// 代码块3
for (var i = 0; i < 5; i++) {
    ((i) => {
        setTimeout(function timer() {
            console.log(i) // 输出 0,1,2,3,4
        }, 0)
    })(i)
}

使用IIFE传递了变量i给匿名函数,IIFE产生了一个新作用域,timer中保留对匿名函数中的i的引用,所以会依次输出。

变体三:

// 代码块4
for (var i = 0; i < 5; i++) {
    (() => {
        setTimeout(function timer() {
            console.log(i) // 输出 5个5
        }, 0)
    })()
}

跟变体2的区别为IIFE没有给匿名函数传递i,timer保留的作用域链中对i的引用还是在全局作用域上。

经过以上两个变体的验证,所以假设B成立,即:由于作用域链的变化,闭包中保留的参数引用也发生了变化,输出的参数也发生了变化。

希望看完的小伙伴可以彻底明白“闭包”和作用域的关系,如果有任何错误请在下方评论区留言,欢迎指正。

推荐文章

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

推荐阅读更多精彩内容