我眼中的js编程(3)--深入理解闭包

函数只能在其所在的作用域内调用吗?怎样在一个函数所在的作用域之外调用该函数?比如下面代码,函数bar定义在函数foo内部,即所在的作用域为函数foo内部,我想在函数foo外部调用bar,直接调用肯定是是报错的,因为外部的作用域访问不到bar。注意,提到函数作用域,要分清楚,函数所在的作用域和函数自身创建的作用域两个概念。因为函数既处在作用域中,同时又创建局部作用域,这两个搞不清下面就没法儿看了。关于函数和变量的声明与访问请看上一篇我眼中的js编程(2)

function foo(){
  function bar(){
    console.log('调用了bar')
  }
}
bar() // ReferenceError

我们可以把bar当做foo的返回值,在bar的作用域外且能访问到foo的地方调用foo,返回bar,然后继续调用。这样就在bar函数所在的作用域外面调用了bar函数。

function foo(){
  return bar
  function bar(){
    console.log('调用了bar')
  }
}
foo()() // 调用了bar

再把代码稍微改一下,猜猜打印a的值是什么呢?

function foo(){
  return bar // 把函数bar作为返回值进行传递
  var a = 3
  function bar (){
    console.log('a的值是',a)
  }
}
foo()() // a的值是 3

我们在bar所在的作用域之外调用了foo,bar作为返回值被传递到这里,紧接着被调用,并且调用的时候bar依然能够访问它所在的作用域--foo中声明的变量。函数记住并且访问了它所在的作用域!

闭包的定义:把一个函数作为值传递时,并且在函数所在作用域之外被调用时,函数可以记住并访问它所在的作用域,也就是说被作为值传递的函数持有它所在作用域的引用,闭包就是这个作用域的引用。

再看一个例子。我们在全局定义一个函数bar,这个函数会调用作为参数传进来的其他函数。

function bar(fn){
  fn()
}

现在我要把一个定义在局部作用域的函数baz作为参数传入bar,通过调用bar来调用baz。这么玩儿肯定报错,因为全局作用域中访问不到baz。

function foo(){
  var a = 5
  function baz(){
    console.log('调用了baz',a)
  }
}
bar(baz) // ReferenceError

但是foo中可以访问全局定义的bar,我们在foo中把baz作为参数传入bar,然后在全局调用foo

function foo(){
var a = 5
bar(baz)
function baz(){
    console.log('baz调用了',a)
  }
}
foo() // baz调用了 5

foo调用的时候,调用了bar,执行baz的调用,函数baz作为参数被传递到bar函数内,然后在函数bar内调用,而baz的作用域是在foo内。函数baz记住并且访问了它的作用域,妈妈,快看呀,闭包又来了!

另外,如果把代码改成

function foo(){
bar(baz)
var a = 5
function baz(){
    console.log('baz调用了',a)
  }
}
foo()

又会输出什么呢?如果把var a = 5换成let a = 5又会怎样呢?答案是undefinedReferenceError,很有趣。如果不明白就复习上一篇我眼中的js编程(2)

再继续看这样的例子

function foo(){
  var a = 5
  setTimeout(function timer(){
    console.log(a)
  },1000)
}
foo() // 1秒钟后打印 5

函数timer以函数表达式的形式定义在foo内,作为参数传递给工具函数setTimeout,setTimeout持有timer的引用并在1000ms后调用timer,而timer定义在foo内部,持有对自身所在作用域即函数foo内部的引用,也就是闭包,所以可以访问到foo内定义的a,1000ms后打印5。

再接着看一个例子

function foo(name){
    document.querySelector('body').onclick = function(){
        console.log(name)
    }
}
foo('hello,closure') // 每次点击页面某位置时候,打印 hello,closure

这次是把一个匿名函数传递给了事件函数,当触发click事件调用该函数的时候,闭包又来了!于是我们可以访问到这个匿名函数所在作用域中的变量name(foo的参数name相当于在foo内部执行var name,调用时候如果有实参'hello,closure',相当于在foo内部最上方执行name = 'hello,closure',验证代码如下)

function foo(name){
  console.log(name)
}
foo() // undefined
foo('hello,closure') // 'hello,closure'

怎么样,看了这么多闭包的例子之后发现规律了吗?一个函数作为值进行传递时,并且在函数所在作用域之外被调用时,就产生了闭包!函数持有的对其所在作用域的引用,就是闭包!不论该函数作值被传递,是以返回值的方式、参数的方式、赋值的方式等等任何方式。在定时器、事件监听器、ajax、跨窗口通信、Web Workers中,都有闭包的身影,你会发现,只要是使用了回调函数,实际上都应用了闭包!

还有一个值得注意的细节。IIFE(立即执行函数)使用闭包了吗?先看这样一个需求,每隔1s输出一次,分别输出1 2 3,看看这样能实现吗?

for(var i=1;i<4;i++){
  setTimeout(function timer(){
    console.log(i)
  },1000 * i)
}

这样执行的结果是,每隔1s输出一次,分别输出 4 4 4。函数timer作为参数传递给setTimeout,持有自身所在的作用域,即全局作用域,在1s 2s 3s分别被调用的时候,for循环执行完毕,全局作用域中的i此时是4。

for(var i=1;i<4;i++){
  (function IIFE(num){
    setTimeout(function timer(){
      console.log(num)
    },1000 * num)  
  })(i)
}

这次实现了,每隔1s输出一次,分别输出 1 2 3。立即执行函数,顾名思义,js引擎解析到IIFE语句时候就会马上执行,执行完毕后,由于垃圾回收机制,IIFE自身创建的作用域在函数调用完毕后会被立即销毁,再次调用,再次创建新的作用域。也就是说,IIFE在for循环的每次迭代中都创建一个新的作用域。这也是在编译之前(写代码的时候)就确定的,只不过在迭代的时候才会创建。

上面的例子说明,IIFE确实创建了封闭的作用域,但它并没有在自身所在作用域之外被调用,而闭包是一个函数在自身所在作用域之外被调用时候持有的对自身作用域的引用,二者都和作用域有关,但是我认为IIFE并没有应用到闭包。普遍认为IIFE是典型的闭包例子,但是我不认同这个观点(参考自《你不知道的javaScript》)。再来看一个例子

var a = 5
(function() foo{
  console.log(a) // 5
})()

很明显,a是通过普通的作用域查找而访问到的。访问a的时候,现在foo函数作用域内查找,找不到a的声明,然后去外层作用域查找。并没有应用到闭包!

现在我们在进一步思考一下刚才的需求。我们实现的方式就是通过IIFE在for循环的{ }内形成了封闭的作用域,每次迭代都会在{ }内形形成新的作用域。等等,{ }内的作用域?这不就是块作用吗?let声明的变量就有块作用域!于是,看下面的代码

for(var i=1;i<4;i++){
  let num = i
  setTimeout(function(){
    console.log(num)
  },1000 * num)
}

用let来代替IIFE形成块作用域,同样也实现了!另外,for循环头部let声明的变量还有一个特殊行为,每次迭代都会被绑定到新的块作用域,这个块就是for(){ }的{ },即for循环头部后面紧跟着的块。所以我们还可以这样写

for(let i=1;i<4;i++){
  setTimeout(function(){
    console.log(i)
  },1000 * i)
}

到此为止,我想我们搞清楚了到底什么是闭包,同时也对比了IIFE、块作用域等知识点。点击查看上一篇我眼中的js编程(2)点击查看下一篇我眼中的js编程(4)
我眼中的js编程系列是我个人的学习总结,如有错误,烦请包涵、不吝赐教,O(∩_∩)O谢谢

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

推荐阅读更多精彩内容