说说简单的闭包

110 阅读2分钟

(开发 || 面试)中经常遇到闭包这个名词,也看到过很多文章介绍, 总是一知半解。今天看到一本书, 关于闭包的描述简单易懂。Mark 一下~

1. 什么是闭包

闭包是另一个函数(外部函数)的内部函数(ps:相信很多面经里面的回答就到此为止了哈= =)。

// 外部函数
function outer() {
  // 内部函数-闭包
  function inner() {}
}

2. 闭包强大的原因

闭包强大的原因在于 闭包函数对作用域链(或作用域层级)的访问。从技术上讲,闭包有三个可访问的作用域:

  • 在它自身声明之内声明的变量。
  • 对全局变量的访问。
  • 对外部函数变量的访问 !important

下面逐一展示下对三个作用域的访问:

  1. code-1: 对自身声明之内声明的变量的访问
    function outer() {
      function inner() {
        // 内部函数中定义变量
        let a = 1;
        console.log(a); // <---- 输出: 1
      }
      inner();
    }
    
  2. code-2: 对全局变量的访问
    // 定义全局变量
    let global = "global";
    function outer() {
      function inner() {
        let a = 1;
        console.log(a);
        console.log(global); // <---- 输出: global
      }
      inner();
    }
    
  3. code-3: 对外部函数变量的访问
    let global = "global";
    function outer() {
      // 外部函数中定义变量
      let outer = "outer";
      function inner() {
        let a = 1;
        console.log(a);
        console.log(global);
        console.log(outer); // <---- 输出: outer
      }
      inner();
    }
    

3. 闭包可以记住它的上下文(作用域)

首先看看下面的栗子:

const fn = function (arg) {
  let outer = "outer";
  function inner() {
    console.log(arg);
    console.log(outer);
  }

  return inner;
};

const callFn = fn(5);
callFn();
/**
 * 输出:
 * 5
 * outer
 */

这个代码很简单,下面分析一下callFn()被调用时,背后发生了什么?

(1) 当下面一行代码被调用时:

const callFn = fn(5);

函数fn被参数5调用,根据函数fn定义, 它返回了一个“内部函数”inner

(2) 当“内部函数”inner被返回时,JavaScript 执行引擎将inner函数视为一个闭包,并相应地设置了它的作用域。根据上一节我们知道,闭包有三个可访问的作用域,这三个作用域链(作用域层级)都是在inner函数返回时设置的。返回函数的引用存储在了callFn中,因此callFn被调用时就记住了inner函数的上下文(作用域)。

(3) 当最后调用callFn()时,便得到了我们预期的结果。

ps: 这里多 bb 两句,我们经常说,闭包会避免外部函数的变量被垃圾回收,这就是因为 JavaScript 执行引擎将外部函数的作用域给到了内部函数, 这样一来, 外部函数内声明的变量存在被引用的状态,当然不会被当做垃圾咯~

4. 闭包的实际应用

各种面试经常问闭包的使用场景有哪些? 这里会涉及到另一个概念:高阶函数。 熟悉了闭包的概念, 相信你也清楚了 JS 中高阶函数的原理。 实际应用中,我们可以利用闭包实现一些真实有用的高阶函数。

这里我只介绍一个小例子哈:

且看一下这个面试题:

["1", "2", "3"].map(parseInt); // 返回值是什么?

哈哈,相信作为大佬的你,一眼就知道结果了,答案是:

[1, NaN, NaN];

具体原因就不说了, 简单来说就是parseInt接收两个参数, 第二个参数跟进制有关。

那么,有什么方法可以正常返回呢?

方案 1: 很简单,只将第一个参数传给parseInt即可。

["1", "2", "3"].map((val) => parseInt(val));

方案 2: 使用函数式编程的思想,封装一个高阶函数,它的任务是将一个给定的多参数函数转为一个只接受一个参数的函数。

/**
 * 1. 通过函数的length属性,可以检查他有多少个参数;
 * 2. 如果只有一个参数, 返回fn
 * 3. 如果有多个参数, 返回一个新的函数,新函数只接收一个参数。
 */
const singleParams = (fn) => (fn.length === 1 ? fn : (arg) => fn(arg));

["1", "2", "3"].map(singleParams(parseInt));

我们看到方案二使用了闭包,因为返回的新函数(arg) => fn(arg)中,函数fn是外部函数singleParams的参数~

换句话说,假入面试官问到了你这个问题, 你使用高阶函数的逼格岂不是有三四层楼那么高~

5. 总结

没啥可总结的,多使用函数式编程的思想来思考问题,对复杂问题进行抽象,将其抑制在我们当前水平,不用关心更复杂的底层逻辑, 开发效率会很高。简单来说就是调包,哈哈哈哈哈哈哈哈哈哈哈哈哈哈