前端必会-闭包

前言

不少同窗都有过,面试的时候被问到闭包,一时间不知道从哪里提及的状况。 其实闭包只是 js 的一种现象(或者说特性),没有想象中的那么可怕。前端

什么是闭包

函数及函数对其词法环境的引用共同组成闭包。 闭包让咱们从内部函数访问外部函数的做用域。vue

那么什么是词法环境呢?react

词法环境

词法环境理解为函数在定义时肯定的做用域环境。 举个例子:面试

function a() {
  var i = 0;
  return function inner() {
    i++;
    return i;
  };
}
// 或者这样
function a() {
  var i = 0;
  function inner() {
    return i;
  }
}
复制代码

内层函数的词法环境就是变量 i 所在的环境。chrome

闭包的 “闭” 理解为:
内层函数所能访问的做用域是在声明的时候肯定的,而不是调用的时候,其词法做用域是对外闭合的。数组

闭包的 “包” 理解为:
内层函数保留对其词法环境的引用,就像是身上背着一个小背包,这个背包里装着对其词法环境中引用的变量,内层函数访问 i 时,在局部做用域中找不到时,就会到背包里看看。markdown

闭包的做用

  • 闭包使函数的私有变量不受外部干扰闭包

  • 是变量存于内存中不被销毁app

举个例子:dom

function a() {
  var i = 0;
  return function () {
    i++;
    return i;
  };
}
var y = a();
y(); // 1
y(); // 2
y(); // 3
复制代码

函数 a 中的变量 i 不能在函数 a 以外被访问。

  • 闭包中的变量 i 保存在哪里?
    保存在父做用域中,每次访问函数 y 时, 在函数 y 中找不到变量 i, 会顺着做用域链一直向上找,直到全局做用域中也没找到为止。

闭包的应用场景

闭包常被见于实现单例模式、柯里化、防抖、节流、模块化

2-0 单例模式

实例仅建立一次、避免重复建立带来的内存消耗

function singleIns(name) {
  this.name = name;
}
singleIns.getInstance = (function () {
  var instance = null;
  return function (name) {
    if (!this.instance) {
      this.instance = new singleIns(name);
    }
    return this.instance;
  };
})();
var a = singleIns.getInstance("a");
var b = singleIns.getInstance("b");
a === b; // true
复制代码

2-1 柯里化

入参可拆解后传入

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      func.apply(this, args);
    } else {
      return function (...args2) {
        curried.apply(this, args.concat(args2));
      };
    }
  };
}
// example
function a(x, y, z) {
  console.log(x + y + z);
}
var b = curry(a);
b(1, 2, 3); // 6
b(1, 2)(3); // 6
b(1)(2, 3); // 6
b(1)(2)(3); // 6
复制代码

2-2 防抖

一段时间内一直触发仅执行一次

// 仅执行最后一次
function debounce(func, time) {
  let timer = null;
  return function (...args) {
    timer && clearTimeout(timer);
    setTimeout(function () {
      func.apply(this, args);
    }, time);
  };
}
// 仅执行第一次
function debounce(func, time){
  let timer = null;
  func.id = 0;
  return function(...args){
    if(func.id !== 0){
      clearTimeout(timer;)
    }
    timer = setTimeout(function () {
      func.id = 0;
      func.apply(this, args);
    }, time);
    func.id = func.id + 1;
  }
}
复制代码

2-3 节流

一段时间内一直触发,每隔固定时间内触发一次

// 时间戳方式
function throttle(func, time) {
  let start = new Date();
  return function (...args) {
    if (new Date() - start >= time) {
      func.apply(this, args);
      start = new Date();
    }
  };
}
复制代码

2-4 模块化

模块化封装中将内部逻辑封装在模块内,将方法暴露到模块外。

function module() {
  var pool = [];
  function add(item) {
    pool.push(item);
    return pool;
  }
  function remove(num) {
    pool.forEach((item, index) => {
      if (item === num) {
        pool.splice(index, 1);
      }
    });
    return pool;
  }
  return {
    add: add,
    remove: remove,
  };
}
var foo = module();
foo.add();
foo.remove();
复制代码

仔细观察上面的例子是否是很容易理解闭包啦?

闭包的坏处

滥用闭包可能会形成内存泄漏(无用变量存于内存中没法回收,一直占用内存)。解决此问题的方法是,清除变量(设为 null)。

function a() {
  var i = 0;
  return function () {
    i++;
    return i;
  };
}
var y = a();
y();
y = null; // 清除变量后,引用消失,闭包就不存在了
复制代码

常见面试题

    1. 考察输出结果
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
复制代码

输出结果为:连续输出 5 个 5 请修改以上代码,使其输出 0、一、二、三、4

方法一: 使用 setTimeout 的第三个参数

for (let i = 0; i < 5; i++) {
  setTimeout(
    function () {
      console.log(i);
    },
    0,
    i
  );
}
复制代码

方法二:使用 let

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
复制代码

let 建立了块级做用域。

方法三:使用自执行函数

for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, 0);
  })(i);
}
复制代码

自执行函数将 i 传递进入一个新的执行上下文。

方法四: 借助函数参数

function output(i) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
for (var i = 0; i < 5; i++) {
  output(i);
}
复制代码

函数传参是按值传递。

方法五:借用 async

const timeout = function (time) {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve();
    }, time);
  });
};
async function print() {
  for (var i = 0; i < 5; i++) {
    await timeout(1000);
    console.log(i);
  }
}
await print();
复制代码
  1. 考察输出结果
var a = 100;
function create() {
  var a = 200;
  return function () {
    console.log(a);
  };
}
var fn = new create();
fn();
复制代码

这题很简单,就是闭包概念的体现,输出 200,这里再也不解释啦。

  1. 实现一个与 sum(x,y)功能相同的 sum(x)(y)函数
function sum(a) {
  return function (b) {
    return a + b;
  };
}
sum(1)(2); // 3
复制代码

考察的是柯里化的概念

tips

是否是全部的闭包都须要手动的清除?

有用的闭包不须要清除,无用的闭包才须要清除。
若是是 dom 事件中的闭包,vue、react 中组件拥有生命周期,卸载时会自动解除 dom 上绑定的事件,内存会自动回收。

开发时不要使用闭包?

闭包无处不在,每当咱们模拟私有变量的时候,闭包就已经产生了。不要让 length 太长的对象和数组一直存于内存当中,合理的使用闭包便可。

如何排查内存泄漏?

闭包并非致使内存泄漏的惟一缘由,借助 chrome 开发者工具(devTools) 的 perfomance 工具 和 memory 工具能够详细观察内存状况。这篇不是讲内存泄漏的,再也不赘述使用方法,推荐学习devtools 官方文档

总结

闭包做为前端八股文之一,难倒了不少正在找工做的同窗。小姐姐如今也在一个学习的过程当中,可能有不许确、不正确的地方,欢迎你们点赞、讨论,一块儿进步呀。但愿对一些同窗有帮助。

本文章为【js 基础】系列文章,关注小姐姐,一块儿学一学。

相关文章
相关标签/搜索