# 闭包

# 闭包的定义

闭包是一个有权访问另一个作用域变量的函数。闭包本身作为一个它外层函数的返回值,可以访问到外界的变量值,但外界访问不到这个函数内部的变量值。当创建这个外层函数实例时,打印出来的是一个 function,也就是创建了返回的这个闭包(内部函数)。

# 闭包的作用(使用场景)

闭包可以实现永久保存局部变量。

  1. 记忆函数
  2. 函数防抖
  3. 函数节流
  4. 设置私有变量(private)
  5. setTimout 内的回调函数
  6. 柯里化函数
  7. IIFE(自执行函数)
  8. 循环赋值(例如 ES5 时期,for 循环保存每轮变量 i)

# 闭包的性质

  1. 闭包可以维持函数内局部变量,使其得不到释放。
  2. 在闭包内,可分为内外两层作用域,内层可以访问到外层(包括闭包之外)的变量,而外层无法访问到内层的变量

# 闭包引发的问题

因 Javascript 的垃圾回收机制没有将闭包内的变量视为 “不再使用的变量”,因此内存不会被释放。且闭包携带着其他函数的作用域,因此会相对其他函数占用更多的内存。如果过度使用闭包则会引发内存泄漏,谨慎使用闭包便可预防这种情况。

# 手写闭包

  1. 一个简单的累加器

    function add(num) {
      let sum = 0;
      return function () {
        sum += num; // 内部sum变量访问外界sum变量
        return sum; // 返回即保存,且不会被释放。
      };
    }
    
    let add5 = add(5); // 创建一个累加5的闭包;
    add5(); // 5
    add5(); // 10
    add5(); // 15
    
  2. 记忆函数

    function memorize(callback) {
      let cache = [];
    
      return function () {
        let key = arguments.length + Array.prototype.join.call(arguments, ",");
        if (key in cache) {
          return cache[key];
        } else {
          return (cache[key] = callback.apply(this, arguments));
        }
      };
    }
    
  3. for 循环输出 i

// 做法1 IIFE
for (var i = 0; i < 5; i++) {
  (function (a) {
    console.log(a);
  })(i);
}

// 做法2
for (var i = 0; i < 5; i++) {
  (function (a) {
    setTimeout(() => {
      console.log(a);
    }, 0);
  })(i);
}
  1. 节流

    function throttle2(callback, delay) {
      let timeout = null;
    
      return function (args) {
        // console.log(timeout);
        if (timeout) {
          console.log("你已在5秒内使用过,稍后再试");
          return;
        }
        console.log("arguments", args);
        timeout = setTimeout(() => {
          callback.call(this, args);
          time = null; // 执行完置空
        }, delay);
      };
    }
    function print(n) {
      console.log("n===", n);
    }
    
    let tPrint = throttle2(print, 5000);
    
    for (let i = 0; i < 5; i++) {
      tPrint(i);
    }
    
    // arguments 0
    // 你已在5秒内使用过,稍后再试
    // 你已在5秒内使用过,稍后再试
    // 你已在5秒内使用过,稍后再试
    // 你已在5秒内使用过,稍后再试
    // n=== 0
    
  2. 闭包经典题(字节)

    var result = [];
    var a = 3;
    var total = 0;
    
    function foo(a) {
      for (var i = 0; i < 3; i++) {
        result[i] = function () {
          total += i * a;
          console.log(total);
        };
      }
    }
    // 在上述循环中,foo函数内的a取得是参数中的a,这当中涉及到作用域,当 当前环境使用到a但没有声明该值时,便会往上一级作用域去寻找。
    // for()中的i变量为全局变量,因此在三次循环完后,i已经变为3。 result[i]中没有做IIFE处理,因此里面的i取的是全局变量i。所以total每次加的值是 3*1, 3*1 , 3*1
    foo(1);
    result[0](); // 3
    result[1](); // 6
    result[2](); // 9
    
  3. 柯里化函数

    function curry(fn) {
      // 获取原函数的参数长度
      const argLen = fn.length;
      // 保存预置参数
      const presetArgs = [].slice.call(arguments, 1);
      // 返回一个新函数
      return function () {
        // 新函数调用时会继续传参
        const restArgs = [].slice.call(arguments);
        const allArgs = [...presetArgs, ...restArgs];
        if (allArgs.length >= argLen) {
          // 如果参数够了,就执行原函数
          return fn.apply(this, allArgs);
        } else {
          // 否则继续柯里化
          return curry.call(null, fn, ...allArgs);
        }
      };
    }
    function fn(a, b, c) {
      return a + b + c;
    }
    var curried = curry(fn);
    curried(1, 2, 3); // 6
    curried(1, 2)(3); // 6
    curried(1)(2, 3); // 6
    curried(1)(2)(3); // 6
    curried(7)(8)(9); // 24