通过讲故事理解JavaScript闭包

306 阅读5分钟
原文链接: www.saojun.top

闭包是 JavaScript 中一个难以理解的概念。简单来说闭包是能够能够读取其他函数内部变量的函数。 例如下面函数b也是一个闭包。

function a(){
  var n = 1;
  function b(){
    var c = 2
    console.log(n)
  }
  b()
}

当函数定义时,函数会有一个作用域链的东西来记录函数可以访问哪些变量,这个作用域分为全局作用域函数作用域。函数作用域只有运行时才会被加入到作用域链中。

函数a 定义时就可以知道他可以访问 window 全局变量 ,当被执行时会将 n 和 b 加入到作用域链中。

作用域链

执行函数 a 时,里面定义了一个b 函数,a 会首先将b变量声明加入内存中,同时b函数被定义时也会产生一条作用域链:

作用域链

当一个函数中没有函数情况时,返回后局部变量就会被销毁。

function a(){
  let n = 1
  return n //返回n后n会被销毁
}

如果一个函数内有函数定义时,内部的函数会添加外部函数能够访问的变量到自身的作用域中。即使这个内部的函数被返回出去了也是能够访问

function say(){
  var name = "zhangsan"
  return function(){
    console.log(name)
  }
}
say()()

执行say()会返回function(){console.log(name)},这个函数会保存在一块内存中,而里面的作用域链则在其定义好了时候就确定好了,即全局作用域链中可以访问 window 和 name 变量。所以在任何地方再次调用那个返回的函数,则都可以访问到 name 属性了。

当然返回一个包含函数的对象也有同样的效果:

function say(){
  var name = "zhangsan"
  return {
    tell:function(){
      console.log(name)
    }
  }
}
var c = say()
c.tell()

如果你还是不理解,我通过一个代码和一个生动的故事来讲解.

一个国王 king 拥有一把密码 key 来掌管他的钱 money,只有国王自己才知道key和money存在什么地方

function king(){
  var key = 'hello,world'
  var money = 10000000
}

可是国王想如果我死了还是希望有人能使用我这笔钱,于是在他卧室里偷偷打造了一个保险箱 box,这个box只有输入正确的密码才能获取到里面的money:

function king(){
  var key = 'hello,world'
  var money = 10000000
  function box(password){
    if(key===password){
      console.log("my money is yours!")
    }
  }
}

国王对 box 说:“你是我打造出来的,我的秘密就是你的秘密,我的key和money对你都公开(作用域机制),你任何时候都可以访问我的 key 和 money,为了防止你找不到,你诞生之初就为你开了一个银行卡(作用域链)”。你可以通过你盒子内壁的[[scoped]]标记去访问这个银行啦。银行里卡号就记录这key和money放在什么地方。

box对国王说:“很好,很合理,即使你死了,由于卡上还是记录着你的key和money信息,根据JS王国银行法律规定,钱财信息不随主人去世而消失。除非世上再也没有和这笔钱有关系的人了(垃圾清除机制)。但是我和钱,密钥都存放在你的私人临时的空间(临时分配的内存)里,假如你死了,根据"JS垃圾清除法",国王死后,他的空间也要被清理。那我你死了,我也会被清除,世上没人和那钱有关系了。那么银行也会自动清除key和money信息了。

国王说:“也是哦,那我死前立下遗嘱,将你送出去 (return出去)。到一个新的空间,那个空间不再属于我了,你就能继续活下去了,不过你要时刻记得,你最初是被谁创造的,你依然还有我的 key 和 money 信息在身上”

function king(){
  var key = 'hello,world'
  var money = 10000000
  return function box(password){
    if(key === password){
      console.log("my money is yours!")
    }
  }
}

国王死之前将 box 转移一个到 c 的地方

var c = king()

国王还特意叮嘱 box,你的价值是保存这笔钱,当有人取走了钱时,你其实已经没有价值了,你最好通知取走这笔钱的人把你捣毁,这样银行也不再保存key和money了,这样就不会占用社会资源了(防止内存泄露)。

var c = king()("hello,world")
c = null

好啦,这故事就讲到这里了,所以你应该明白:

1.闭包就是函数里面创建一个函数,那个函数能访问到外部的变量
2.函数里面一般 return 出这个闭包函数,return 时候闭包会保存到一个新的地址
3.即使 return 出去保存到新的地方,他也会保存最初定义它的地方的变量
4.闭包不使用要记得销毁哦。

下面说说闭包的应用。

1 解决经典的循环打印问题

for(var i=0;i<6;i++){
  setTimeout(()=>{
    console.log(i)
  },200)
}
// 全部输出6

由于setTimeout是异步的,不会立即执行,当循环i已经变成了6,再去执行console.log(i)自然就都会打印6了。 结合刚刚闭包例子,我们需要循环时候就先保存好i,将它加到作用域链中

function aa(i){
  return function(){
    setTimeout(()=>{
      console.log(i)
    },200)
  }
}

解决办法

  1. 给执行语句外面加一层函数,由作用域规则,aa函数被定义时候i的值就被确定了,所以console.log(i)会优先使用参数中的i
for(var i=0;i<6;i++){
  function aa(i){
    setTimeout(()=>{
      console.log(i)
    },200)
  }
  aa(i)
}

//可以简写成:
for(var i=0;i<6;i++){
  (function(i){
    setTimeout(()=>{
      console.log(i)
    },200)
  })(i)
}

方法二 setTimeout第一个参数传入一个闭包

for(var i=0;i<6;i++){
  let func = function(i){
    return ()=> console.log(i)
  }
  setTimeout(func(i),200)
}

应用2 模块封装

例如实现统计对象$方法被调用次数

var $ = (function(){
  var count = 0
  return {
    add(a,b){
      count++
      return a+b
    },
    print(){
      count++
      return count
    }
  }
})()
$.add()
console.log($.print())

当然还有许多应用,例如实现节流防抖方法,实现缓存和单例模式等。

完!