我眼中的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谢谢

你可能感兴趣的:(我眼中的js编程(3)--深入理解闭包)