【面试题解】初识 JavaScript 闭包

5,076 阅读4分钟

前言

本文很基础,适合没有了解过闭包的同学入门,我是经常使用 js 书写业务逻辑,但没有刻意使用过闭包,如果你的情况跟我差不多,那么跟着本文,你一定也可以有所收获。

进入今天的主题 闭包

闭包

闭包是什么

闭包(closure)JavaScript 的难点,也是它的特色。是号称 JS 面试三座大山(原型与原型链作用域及闭包异步和单线程)其中的一座山。

很多高级应用都需要依靠闭包来实验,包括我们去看很多的 JS 库和框架的源码,都少不了闭包的影子。

定义

闭包就是能够读取其它函数内部变量的函数。

为什么我们要借助闭包来 读取其它函数内部的变量 呢?

因为 JavaScript 这个语言的特别之处就在于,函数内部 可以直接读取 全局变量 ,但是在 函数外部 无法直接读取 函数内部局部变量 。只有 函数内部子函数 才能读取 局部变量 ,可以看下面的例子。

// 此部分只为演示全局变量和局部变量 与闭包无关
  // 全局变量 在任何地方都可以访问
  let s = 100;
  function foo() {
    // 局部变量,函数运行时创建,函数执行完销毁
    let a = 10;
    function boo() {
      console.log('🚀🚀~ boo:', a); //🚀🚀~ boo: 10
    }
    boo()
  }
  foo()
  console.log('🚀🚀~ s:', s); //🚀🚀~ s: 100
  console.log('🚀🚀~ a:', a); // Uncaught ReferenceError: a is not defined

所以说,闭包可以简单理解成 定义在一个函数内部的函数 。闭包本质上,就是将 函数内部函数外部 连接起来的桥梁。

如何从外部读取函数内部的局部变量

先来思考一个问题。如何从 函数外部 读取 函数内部局部变量 ?可是前面不是已经说了么,在 函数外部 无法直接读取 函数内部局部变量

是的,确实无法 直接 读取,但是我们可以 变通 一下。

第一种是 return 返回。


  function foo() {
    let a = 88;
    return a;
  }
  console.log('🚀🚀~ : a', foo()); // 🚀🚀~ : a 88

第二种是上面提到的子函数。

  function foo() {
    let a = 99;
    function boo() {
      console.log('🚀🚀~ a:', a);
    }
    boo();
  }
  foo();// 🚀🚀~ a: 99

这里先留下一个思考题。

  • 根据闭包的定义,闭包就是能够读取其它函数内部变量的函数,那么以上两种方式是闭包么?如果不是,他们都能拿到局部变量的值,并且更简单,为什么还要用闭包呢?

为什么需要闭包

局部变量在函数执行时被创建,函数执行完被销毁,没有办法 长久的保存状态共享

全局变量可能造成 变量污染 ,使代码变得难以阅读,难以维护。

那么我们就希望有一种 即可以长久的保存变量又不会造成全局污染 的操作,闭包也就应运而生了。

闭包的写法

  function f1() {
    let a = 10;
    function f2() {
      a++;
      console.log('🚀🚀~ a:', a);
    }
    return f2;
  }
  let fn = f1(); // f1执行的结果就是闭包
  fn()

思考题解答

现在我们就来解答一下刚才留下的思考题,子函数 和直接 return 也能拿到局部变量的值,为什么还需要闭包呢。

  //闭包
  function f1() {
    let a = 10;
    function f2() {
      a++;
      console.log('🚀🚀~ a:', a);
    }
    return f2;
  }
  let fn = f1(); 
  fn();
    
  //直接return
  function f3() {
    var a = 10;
    a++;
    return a
  }
 console.log('🚀🚀~ a:',  f3());
  
  //子函数
  function f4() {
    let a = 10;
    function f5() {
      a++
      console.log('🚀🚀~ a:', a);
    }
    f5();
  }
  f4();

可以看到控制台输出的结果是一样的。

image.png

那么我们多调用几次呢?

  //闭包
  function f1() {
    let a = 10;
    function f2() {
      a++;
      console.log('🚀🚀~ 闭包 ~ a:', a);
    }
    return f2;
  }
  let fn = f1(); // f1执行的结果就是闭包
  fn();
  fn();
  fn();
  fn();

  //return
  function f3() {
    let a = 10;
    a++;
    console.log('🚀🚀~ return a:', a);
  }
  f3();
  f3();
  f3();
  f3();


  //子函数
  function f4() {
    let a = 10;
    function f5() {
      a++
      console.log('🚀🚀~ 子函数 a:', a);
    }
    f5();
  }
  f4();
  f4();
  f4();
  f4();

发现什么了么,我们使用闭包,每次调用后,变量 a 的值都会 +1 ,而我们直接 return 以及 子函数 的方式,每次调用后,变量 a 的值一直都是 11

image.png

到这里之前留下的思考题就已经有答案了。闭包是一个能够读取其它函数内部变量的函数,但是能够读取其它函数内部变量的函数不一定就是闭包,为什么需要闭包,因为闭包 即可以长久的保存变量又不会造成全局污染

闭包的缺点

优点上面已经说过了,那么闭包有什么缺点呢。通常情况下,函数的活动对象会随着执行的上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,这意味着闭包比普通函数要消耗更多的内存。

案例-缓存

  const cacheMemory = (() => {
    let cache = {}
    return {
      set: (id) => {
        if (id in cache) {
          return `查找到的结果是${cache[id]}`
        }
        const result = asyncFn(id);//模拟异步结果
        cache[id] = result
        return `查找到的结果是${cache[id]}`
      }
    }
  })()

案例-模拟栈

  const Stack = (() => {
    let arr = [];
    return {
      push: (value) => {
        arr.push(value)
      },
      pop: (value) => arr.pop(value),
      size: () => arr.length,
    }
  })()

  Stack.push("a")
  Stack.push("b")
  console.log('🚀🚀~ Stack.size:', Stack.size()); // 2
  console.log('🚀🚀~ Stack.pop("b"):', Stack.pop("b")); // b
  console.log('🚀🚀~ Stack.size:', Stack.size()); // 1

参考

前端面试题讲解