专注收集记录技术开发学习笔记、技术难点、解决方案
网站信息搜索 >> 请输入关键词:
您当前的位置: 首页 > JavaScript

小弟我理解的闭包

发布时间:2010-05-20 14:01:29 文章来源:www.iduyao.cn 采编人员:星星草
我理解的闭包

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

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

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

可能和同学们平常看到的理解不太一样,但维基百科的确是这样描述的:

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)
闭包就是一个引用了外部变量的函数以及其运行环境的统称

平常我们提到的“闭包”,都是通过返回一个函数来让外部能访问函数内部的变量,但我认为这只是闭包的一种应用罢了。前面的例子是闭包,但是它已经是全局环境了,不能再被指向给上一层环境,因此函数执行完成后该闭包便被销毁了。

下面让我们以一个简单的计数器函数为例,更加深入的去理解闭包:

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

/*使用闭包*/
let add = function plus ()  {
  let count = 0
  return function closure () {
      return count += 1
  }
}()
add() // 1
add() // 2
add() // 3

不使用闭包的情况下,每次执行add()函数时,局部变量count值都会被初始化为0,并不能起到计数器的作用。

使用闭包的情况,本身是两个匿名函数,为了方便描述我给它们分别命名为plus()和closure()。plus内定义了一个变量count,然后返回了closure函数,这个函数引用了count变量。函数最后一个()让其执行后,将其结果赋值给了一个全局变量add。

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

让我们看一下闭包是怎样工作的:closure函数引用了其外部变量count,此时closure函数(以及其运行环境和外部变量)形成一个闭包,并将整个闭包返回,赋值给了一个全局变量。于是整个闭包随着add留在内存中,直到add与该闭包的连接被清除(将add指向别处或者 add = null),该闭包占用的内存才会被回收。

闭包的应用1

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

(function (global, factory) {
  ...
})(window, function (window) {
  var arr = []
  var document = window.document
  ...
  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)

闭包的应用2

平常我们可能需要在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机制回收,因此当不再使用该闭包时,应当及时将该闭包与当前环境的连接清除,以便内存被系统回收。

4楼南栀夏沫
写的很详细啊,赞一个
3楼AdamAG
说的很好
2楼秋风扫落叶2
学习了
1楼cdx71666344
感谢分享
友情提示:
信息收集于互联网,如果您发现错误或造成侵权,请及时通知本站更正或删除,具体联系方式见页面底部联系我们,谢谢。

其他相似内容:

热门推荐: