闭包是什么?一文了解闭包

148 阅读4分钟
闭包是什么?

什么是闭包,要真正的理解闭包我们需要就要先去理解作用域。

作用域

什么是作用域?就是这个变量和函数在程序中使用的范围。作用域分为函数作用域(局部作用域)和全局作用域。

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域。

  • 优点是可以重复的使用
  • 缺点是全局污染,容易出现错误,变量容易出现被其他的地方窜改,代替的问题。

如图,在浏览器的调试scope中三种类型的值都在全局的scope中。

2.png

函数作用域

在函数内部创建的作用域,其内部的变量和函数对外不可见

function foo() {
	var a = 1
}
foo()
console.log(a) // ReferenceError: a is not defined
作用域链
  • 一个函数可用的所有作用域串联起来, 就形成了当前函数的作用域。
  • 使用解释: 当在javascript中使用一个变量的时候,首先javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上级作用域寻找,以此类推直到找到该变量或是已经到了全局作用域,如果在全局作用域里仍然找不到该变量,它就会直接报错。
var a = 100 
function foo() {
  var b = 200
  console.log(a) // 100
  console.log(b) // 200
  console.log(c) // ReferenceError: c is not defined
}
foo()
闭包是什么?
  • 1.小'黄'书(你不知道的javascript): 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
  • 2.红宝书(javascript高级程序设计): 闭包是指有权访问另一个函数作用域的变量的函数
  • 3.MDN对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。
形成闭包的原因

内部的函数存在外部作用域的引用就会导致闭包。

闭包的优缺点
  • 闭包的优点

    1. 保护变量:闭包可以将变量封装在函数内部,避免全局污染,保护变量不被外部访问和修改。
    2. 延长变量生命周期:闭包使得函数内部的变量在函数执行完后仍然存在,可以在函数外部继续使用。
    3. 返回函数:闭包通常以函数的形式返回,使得外部函数的变量仍然可以被内部函数引用和使用。
  • 闭包的缺点

    1. 内存占用:闭包会导致外部函数的变量无法被垃圾回收,从而增加内存占用。如果滥用闭包,会导致内存泄漏问题。
    2. 性能损耗:闭包涉及到作用域的查找过程,会带来一定的性能损耗。
闭包使用的场景
    1. 使用return 返回函数
    var n = 10
    function foo() {
      var n = 50
      return function fn() {
        n++
        console.log(n)
      }
    }
    var f = foo()
    f() // 51
    f() // 52
    

    这里的return f, f()就是一个闭包,存在上级作用域的引用,闭包函数执行完后外部作用域变量仍然存在,并保持状态。

    1. IIFE自执行函数
var foo = (function () {
  var n = 0
  return function() {
    ++n;
    return `id_${n}`
  }
})()
console.log(foo());

在IIFE之外无法访问函数内部的n变量,除了从IIFE中返回的函数,别处无法读写该变量,这样就能创建真正的私有状态变量。

    1. 函数作为参数
function foo() {
  var a = 'foo'
  function fn() {
    console.log(a)
  }
  return fn
}
function f(p) {
  var a = 'f'
  p()
}
f(foo)

return fn就产生闭包, f(foo)执行的是fn, fn的上级作用域是函数foo(), 所以输出就是foo

    1. 循环赋值
for(var i = 0; i < 5; i++) {
  (function(j){
    setTimeout(function() {
      console.log(j)
    }, 1000)
  })(i)
}

存在闭包,上面依次输出1-5,闭包形成了互不干扰的私有作用域。

    1. 使用回调函数就是在使用闭包
  var name = 'foo'
  setTimeout(function timeHandler(){
    console.log(name);
  }, 100)
  • 6. 节流防抖

防抖(debounce)就是在一定时间内,如果连续触发事件, 则只会执行最后一次事件的回调函数。 节流(throttle)就是连续触发的事件限制在一个特定的时间段内,在这个时间段内只执行一次回调函数。

1. 防抖
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    if(timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arg)
    }, delay)
  }
}

2. 节流
function throttle(fn, delay) {
  let timer = null
  return function (...args) {
    if(timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}
  • 7. 函数柯里化

函数柯里化(currying)是一种将多个参数的函数转换为一系列接受单个参数的函数的过程。目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用

  function curry(fn, len=fn.length) {
    return _curry(fn, len)
  }

  function _curry(fn, len, ...arg) {
    return function(...params) {
      let _arg = [...arg, ...params]
      if(_arg.length >= len) {
        return fn.apply(this, _arg)
      }else {
        return _curry.call(this, fn, len, ..._arg)
      }
    }
  }

  let fn1 = curry(function(a, b, c, d, e) {
    console.log(a+b+c+d+e)
  })

  fn1(1)(2)(3)(4)(5)
  fn1(1, 2, 3, 4, 5) // 15
  function getArea(width) {
    return function(height) {
      return width * height
    }
  }
  const areaWidth = getArea(10)
  const areaHeight = areaWidth(20)
  console.log('areaHeight :>> ', areaHeight);