【面试题解】初识 JavaScript 闭包

5,284 阅读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

参考

前端面试题讲解