JS原理 - 理解闭包

闭包的概念以及相关知识点

概念

闭包的本质就是:当前环境存在指向父级作用域的引用。

在函数内部声明一个新的函数(即内部函数),内部函数引用了其父函数的变量,并将内部函数作为值返回(return),在父函数被调用时就会形成闭包。

function foo(){
    var name = 'lili';  // 父函数的局部变量
    function child(){
        console.log(name); // 引用父函数中声明的变量
    };
    return child; 
}

const x = foo();
x();

是不是只有返回一个函数的形式的才是闭包呢?

不,只需要存在指向父级作用域的引用的即可,举个例子:

var fun3;
function fun1() {
  var a = 2
  fun3 = function() {
    a++;
    console.log(a);
  }
}
fun1();
fun3(); // 3
fun3(); // 4

作用

访问内部变量

闭包可以保持对作用域的引用,让外部函数能够访问内部函数的变量。

打个比方,有一根金条放在邻居家里,你不能入室去偷这根金条,但是你可以通过贿赂邻居家的小孩,让他帮你把金条从房子里拿出来,你就得到这根金条了。

function foo(increment){
    let counter = 0;
    function child(){
       counter += increment;
       console.log(counter);
    };
    return child; 
}

const _child = foo(1);
_child(); // 1
_child(); // 2
_child(); // 3
console.log(counter); // error:counter is not defined

如果直接打印 counter 一定会得到counter is not defined,因为无法访问 foo 函数内部的变量,但通过闭包的形式,却可以打印出变量 counter 的值。

保留变量的引用

而且 counter 能够实现累加,因为闭包保持了 foo 作用域中的变量(counter)和传入的参数(increment)的引用,除非手动销毁,不然作用域会一直保存在内存当中。

然后来看一个特殊情况,在返回的 child 函数中再套一个函数 inner,inner 打印出 child 函数里的变量 msg:

function foo(increment){
    let counter = 0;
    function child(){
       counter += increment;
       console.log(counter);
       const msg = `counter is ${counter}`;
       return function inner(){
           console.log(msg);
       }
    } 
    return child; 
}

const _child = foo(1);
const _inner = _child(); // 1
_child(); // 2
_child(); // 3
_inner(); // counter is 1

从下图可以看到,出现了两个 Closure(闭包):

如果要让 counter 获取到最新的值,有两种解决方式:

  1. 改变调用_child()的位置:

    function foo(increment){
        let counter = 0;
        function child(){
           counter += increment;
           console.log(counter);
           const msg = `counter is ${counter}`;
           return function inner(){
               console.log(msg);
           }
        } 
        return child; 
    }
    
    const _child = foo(1);
    _child(); // 1
    _child(); // 2
    const _inner = _child(); // 3
    _inner(); // counter is 3
    

    这样虽然还是产生了闭包,但是闭包内的 counter 值是最新的。

  2. 将 msg 移入至 inner 函数内,这样 child 函数和 inner 函数就不存在引用关系,就无法形成闭包。

    function foo(increment){
        let counter = 0;
        function child(){
            counter += increment;
            console.log(counter);
            return function inner(){
                const msg = `counter is ${counter}`;
                console.log(msg);
            }
        }
        return child;
    }
    
    const _child = foo(1);
    const _inner = _child(); // 1
    _child(); // 2
    _child(); // 3
    _inner(); // counter is 3
    

    从下图可以看到,在运行最后的 _inner() 时,并没有形成两个 Closure(闭包):

从上述的例子可以看出,并不是所有的函数都能形成闭包,需要子函数有父函数变量的引用才行。

实际运用

防抖节流

function debounce(fn, delay = 300) {
  let timer; //闭包引用的外界变量
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
作者

BiteByte

发布于

2020-08-08

更新于

2024-11-15

许可协议