拒绝抄书,完全消化闭包

前言

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

ok,如今开始死抠。什么是闭包?java

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

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

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

词法做用域

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

图示

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

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

咱们看下面这几句代码:浏览器

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. 深刻理解闭包以前置知识---做用域与词法做用域
相关文章
相关标签/搜索