我理解的闭包

网上关于闭包的文章一搜一大堆,但是我还是要来说一下我的理解。

我理解的闭包,其实就是访问了外部变量的函数

let a = 0
function b() {
  console.log(a)
}

这里的函数b,实际上已经是一个闭包!
可能和大家理解的不太一样,但维基百科里对于闭包的描述的确是这样的:

a closure is a record storing a function together with an environment: a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope)
闭包就是一个引用了外部变量的函数以及其运行环境的统称

平常我们看到的“闭包”,都是通过返回一个函数来让外部能访问函数内部的变量,但我认为这是闭包的一种应用罢了,而不是闭包本身。前面的例子是闭包,但是函数执行完成后该闭包便被销毁了。因此平常看到的return一个闭包到外层并赋值给一个外部变量,或者直接将外部变量指向内部函数,都是为了让闭包不被GC机制回收,并且在外部访问内部变量。

仅仅理解闭包本身是不够的,还是得说到闭包的应用,下面是一个简单的计数器例子:

闭包的应用1

/*不使用闭包*/
function add1 () {
  let count = 0;
  return counter += 1;
}
add1() //1
add1() //1
add1() //1

/*使用闭包*/
let add2 = function plus ()  {
  let count = 0
  return function closure () {
      return count += 2
  }
}()
add2() // 2
add2() // 4
add2() // 6

每次执行add1()函数时,局部变量count值都会被赋值为0,并不能起到计数器的作用。

再看add2,plus内定义了一个变量count,然后返回了closure函数,这个closure引用了count变量。最后自执行plus,将closure函数赋值给了变量add2 --- closure函数和其引用的外部变量count形成了一个闭包,并指向了更外层的变量add2。如果add2没被销毁,这个闭包也不会被销毁,于是count变量能够保存每次增加后的值。

下面我们看下调用add2函数会发生什么:调用add2函数即调用closure,执行count+=2,而closure函数内部是没有定义‘count’这个变量的,于是它会循着作用域链往上查找,到plus 函数找到了count变量,然后取得count的值,计算并返回。再次执行,可以看到count值是继续递增的,说明count被保存在了内存中,但却不能直接访问。

闭包的应用2

闭包最常见的应用,是各种js库用来封装源码。以jQuery为例,源码核心结构如下:

(function (global, factory) {
  ...
})(window, function (window) {
  var arr = []
  var support = {}
  ...
  var jQuery = function (selector, context) {
    ...
  }
  ...
  window.jQuery = window.$ = jQuery
})

我们来解读一下这里的闭包:首先,匿名函数内定义了各种变量,然后jQuery对象(同时也是一个函数)及其属性和方法引用到了这些变量。最后用window.$ = jQuery将jQuery对象和全局对象连接起来---因此这个闭包只会在其全局对象被销毁(页面或iframe被关闭),或者连接被切断(window.$ = null)时才会销毁。这就是闭包最常见的应用---利用匿名自执行函数的作用域,将内部变量封装起来,防止被外部修改,也避免了污染全局变量环境。

看到这里也明了,并不是“return一个函数才叫闭包”,前面计数器的例子也可以改成这样:

(function closure(global) {
  let count = 0
  function plus() {
    return count += 1
  }
  global.add = plus
})(window)

闭包的应用3

平常我们可能需要在window.onresize中改变页面样式,用户输入字符时ajax远程搜索等。由于这类事件会在短时间内多次触发,不加以控制则会频繁调用处理程序,影响性能。我们可以利用闭包,来实现函数节流(throttle)和函数去抖(debounce),提高页面性能。

函数节流:预先设定一个执行周期,当调用方法的间隔大于执行周期则执行该方法,然后记录当前执行的时间并进入下一个新周期。

const throttle = function (fn, ms) {
  let timestamp = 0
  return function () {
    let current = Date.now()
    if (current - timestamp > ms) {
      fn.apply(this, arguments)
      timestamp = current
    }
  }
}

setInterval(throttle(function (arg) {
  console.log(arg)
}, 2000).bind(this, 'hello'), 50)

利用闭包将timestamp 的值保存起来,以记录函数上次的调用时间。如果小于时间间隔则不处理,如果大于间隔则执行函数并记录这一次执行的时间。我们用setInterval模拟一下连续触发的情况,可以看到虽然setInterval的间隔设置为50,但是函数的执行间隔仍然是由throttle设定的间隔控制的---2s触发一次。这个方法也适用于防止用户连续点击按钮发起重复请求的情况。

函数防抖:有一个形象的比喻是,如果用手指一直按住一个弹簧,它将不会弹起,直到你松手为止。也就是说当调用函数n毫秒后,才会执行该函数,若在这n毫秒内又调用此函数,则将重新计算时间。

var debounce = function(fn, ms){
  var timeoutID
  return function(){
    clearTimeout(timeoutID)
    timeoutID = setTimeout(() => {
      fn.apply(this, arguments)
    }, ms)
  }
}

setInterval(debounce(function (arg) {
  console.log(arg)
},300).bind(this,'world'),50)

这次我们用闭包保存了setTimeout返回的ID。每当函数执行的时候,就重新设置timeout,因此50ms间隔的interval遇上300ms间隔的debounce,函数将永不会执行,直到取消interval。这种方法适用于页面resize,文字输入远程搜索等情况,但是由于是延迟触发,不适合用于按钮点击等交互体验明显的地方。

总结

作用域的限制,让外部环境不能访问内部变量,而内部环境可以访问其作用域链上的外部变量;当一个函数访问了其外部变量,这个函数就同这些被访问的变量形成了闭包;如果将这个闭包同外层环境连接起来,这个闭包将会一直存在内存中,直到外层环境销毁;而外层环境可以利用闭包函数,间接访问闭包中保存的变量。

由于闭包会让函数及变量一直留在内存中,不能被GC机制回收,因此当不再使用该闭包时,应当及时将该闭包与当前环境的连接清除,以便内存被系统回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值