浅谈我对闭包的理解

102 阅读5分钟

1基本概念

  • 由于 JavaScript 中下级作用域可以引用上级作用域中的变量,所以在某些情况下当我们需要创建私有变量的时候就可以采用闭包的形式来促使其不被垃圾回收机制回收。

2 作用域

2.1 变量查找
  • 在 JavaScript 中下级作用域可以访问上级作用域中的变量(此处的作用域指的是函数作用域和全局的上下文),如下在函数 fn 中可以访问到全局变量 b,在函数 fn1 中可以访问到 fn 中定义的变量 a、b。
  • 在使用变量的时候会一级一级向上查找,如果自身没有会向上一级中查找,一直找到全局上下文,如果全局中也不存在会返回 undefined
  • 函数外部不能访问函数内部的变量
let b = 1
let c = 1
function fn() {
    let a = 2
    console.log(b) // 输出1
    let c = 3
    function fn1() {
        console.log(a) // 输出2
        console.log(c) // 输出3 
        console.log(d) // 报错undefined
    }
    fn1()
}
fn()
console.log(a) // 报错undefined
2.2 垃圾回收机制
  • 在 JavaScript 中当一个变量被声明的时候他就被创建了,此时如果他的值不为 null 的话那么他就会一直存在于上下文当中,一直到上下文被解除引用时这个变量也会跟着被解除从而会在下一次垃圾回收器执行的时候被回收。

  • 同样我们也可以主动将变量的值设置为 null 这样就是解除引用会使其在下一次垃圾回收中被回收

    let a = 1 // 此时a为全局变量除非window对象不存在了才会被解除引用let b = 2 // 声明并赋值
    b = null // 解除引用
    
2.3 函数的作用域
  • 当函数被声明时并不会立即创建函数的作用域,而是当函数被调用时才会创建函数的作用域,当函数执行完毕后它的作用域也就失去了引用

  • 每次函数调用都会开辟一个新的作用域,和上一次函数调用时的作用域完全无关

    function fn(){
        let a = 1
        console.log(++a)
    }
    fn() // 输出2
    fn() // 输出2
    fn() // 输出2
    fn() // 输出2
    // 在这无论调用多少次fn输出的都是2,因为对于fn来说每次调用他都会重新声明一个a并赋值为1,所以每次的打印都是 ++1也就是2
    

3 闭包

  • 先来看一个典型的闭包例子
// 定义一个函数fn,在其内部再定义一个函数fn1,将函数fn1作为fn的返回值返回出去
function fn(){
    let a = 1
    function fn1(){
        console.log(a++)
    }
    return fn1
}
// 将fn的返回值赋值给变量b,此时fn的返回值就是函数fn1,由于b保持着对fn1的引用所以当fn执行完并返回值的时候其作用域并不会被解除引用,这时就形成了一个闭包(私有变量空间)
let b = fn()
// 执行b的时候由于并没有重新执行函数fn并且fn的作用域被保留了所以每次执行b时a都自加且变化了,那么a就可以看作是b的外部变量所以会累加
b() // 输出2
b() // 输出3
// 定义变量c的时候由于重新执行了fn函数所以对于c来说这就是一个新的作用域,和之前的b没有关联所以执行c的时候变量a会重新从1开始累加
let c = fn()
c() // 输出2
  • 对闭包来说最重要的一点就是需要有外部引用来保证内部环境的引用从而是得其父级环境被维持从而保证被引用

    function fn(){
        let a = 1
        function fn1(){
            console.log(a++)
        }
        return fn1
    }
    // 在这里因为没有使用全局变量保持对函数fn中变量的引用所以在fn1执行完之后函数fn的作用域就不再存在了,当然这里也是因为第二次调用的时候重新执行了fn函数导致创建了一个新的作用域
    fn()() // 输出2
    fn()() // 输出2
    

5闭包的作用

  • 可以用来保存某个函数的执行结果用于之后的操作
  • 可以在函数外部访问函数内部的变量
  • 常见的闭包应用一般有函数柯里化、按钮的防抖节流等

6 闭包带来的问题

  • 因为闭包就是为了维持某个函数的作用域始终存在,所以当你在闭包中如果维持了一个很大的数据在里面,那么在 window 对象存在的期间他就会一直存在,而如果你在很多地方都是用了闭包就会占了很大的内存空间,容易造成内存泄露。所以要慎用闭包,当然我们也可以通过取消对引用闭包作用域的对象的引用来使得垃圾回收器在执行的时候会将其回收
function fn(){
    let a = 1
    function fn1(){
        console.log(a++)
    }
    return fn1
}
let b = fn()
fn()
b = null // 当你在不需要b的时候可以将b的值置空,那么b就会在下一次垃圾回收的时候被销毁,那么函数fn也就失去了引用会被一同回收

7 闭包的this指向问题

  • 函数中的 this 指的都是函数的拥有者
function fn(){
    console.log(this) // 输出window
}
fn()
  • 在闭包中,this的指向指的是引用闭包的对象的拥有者

    function fn(){
        function fn1(){
            console.log(this) 
        }
        return fn1
    }
    fn()()// 输出window
    
    let a = fn()
    a() // 输出window(这里是因为变量不是fn()的所有者,只是保持了对fn()的引用,所以fn()的所有者应该是a的所有者也就是windowlet obj = {
        fn:fn()
    }
    obj.fn() // 输出obj对象
    
  • 如果我们想在闭包中让this指向和父级函数一致的话可以使用箭头函数或者用一个变量来保存this

    function fn(){
        return () => {
            console.log(this) 
        }
    }
    
    function fn1(){
        let that = this
        function fn2(){
            console.log(that) 
        }
        return fn2
    }
    
    let obj = {
        fn:fn(),
        fn1:fn1()
    }
    obj.fn() // 输出window
    obj.fn1() // 输出window
    

    \