maicFir / lessonNote

JS学习笔记
33 stars 11 forks source link

title: 掌握闭包,夯实基本功 #12

Open maicFir opened 2 years ago

maicFir commented 2 years ago

闭包在程序中无处不在,通俗来讲闭包就是一个函数对其周围状态的引用并捆绑在一起的一种组合,你也可以理解成一个函数被引用包围或者是一个内部函数能够访问外部函数的作用域

闭包是面试经常考的,也是了解一个程序员基础知识一个重要点,本篇是笔着对于闭包的理解,希望在实际项目中有所思考和帮助。

正文开始...

闭包是什么

我们可以从以下几点来理解

  1. 闭包是一个函数对其周围状态引用并捆绑在一起的一种组合
  2. 一个函数引用包围
  3. 一个内部函数能访问外部函数作用域

我们来看一张图理解下上面三句话

对应代码如下

function A() {
  var name = 'Maic',
    age = 0;
  function B() {
    console.log(name, age);
  }
}
A();
// console.log(name) name is not defined

我们注意到在A函数中,我们创建了两个内部的私有变量nameage,并且我们在A函数中创建一个内部函数B,此时在B函数中,我们会发现在B内部可以访问它周围状态(变量),也就意味着在B函数内部可以访问外部函数的作用域。

至此你会发现,闭包就是在B函数一创建,并且有对周围的状态有引用,那么此时闭包就出现了,也就是说,闭包就是一座桥梁,能让B函数内部能突破自身作用域访问外部的变量。

不知道你有没有发现,我在A内部定义的变量,我在外部并不能访问,也就是说相对A的外部,A内部所有的变量都是私有的,在A定义的变量,相对于B中,又可以访问。因为B函数能访问A中的变量,也正是依靠闭包这座桥梁。

闭包的特性

1.创建私有变量

2.延长变量的生命周期

我们知道闭包会造成内存泄露,本质上就是创建的变量一直在引用内存中,当一个普通函数被调用结束时,函数内部创建的变量就会被销毁。

但是闭包会保存其变量的引用,即便外部执行上下文被销毁,但是闭包内创建的词法环境依然还在,我们看下面代码具体理解下。

function A() {
  var name = 'Maic',
    age = 0;
  function B() {
    age++;
    console.log(name, age);
  }
  return B;
}
var b1 = A();
b1(); // 1
b1(); // 2
b1(); // 3

A中返回了函数B,实际上就是返回了一个函数,当我们我们用var b1 = A()申明一个变量时,实际上,这里内部B还没有执行,但是在执行A()方法时,返回的是一个函数,所以我们继续执行b1(),我们尝试调用三次,我们会发现打印出来的值是1,2,3,这就说明,闭包延长了变量的生命周期,因为第三次与第二次打印出来的值就是同一个值的引用。 具体一张图可以可以理解下

当我们用var b1 = A()时,实际上,我用蓝色的方框已经标注起来了,在b1内部我们可以看到,每执行b1,实际就是执行的红色区域的函数,也就是A内部定义的函数B,但是每次调用b1,我们看到都有保留age的引用,所以你看到age依次就是1,2,3,所以也就证实了闭包能延长变量的生命周期,并且闭包创建的私有变量可以减少全局变量的使用。

通常我们知道尽量少创建全局变量,因为我们不知道这个全局变量什么时候使用,只有在被使用的时候才会被释放。闭包也是解决了全局变量命名冲突的问题,因为创建的私有变量,没法在外部访问,这样也就减少了变量名污染的问题。

等等,还有一个问题,如果我把上面的代码改成下面呢?

function A() {
  var name = 'Maic',
    age = 0;
  function B() {
    age++;
    console.log(name, age);
  }
  return B;
}
A()(); // 1
A()(); // 1
// var b1 = A();
// b1();
// b1();
// b1();
// console.log(name)

你会发现,我两次打印的都是同一个1,这是为什么呢?你有没有发现之前我们是用var b1 = A()申明的一个变量,实际上这句代码就是js新开辟暂存了一块空间,因为A内部返回的是一个函数,当我每次调用b1时,实际上是调用返回的那个函数,因为函数内部存在闭包的引用,所以一直就1,2,3,但是我这里我使用的是A()(),我们发现每次都是1,说明当我第二次调用时内部的age已经重新定义了一遍,而并没有引用上一次的值,这就说明,在A()立即调用时,闭包内部引用的变量已经被释放。由于闭包也会有缺陷,创建太多的闭包会造成消耗内存严重,影响网页性能。

应用场景

  1. 柯里化函数

  2. 回调函数

  3. 计数器延迟调用(防抖与节流)

实际上就是把一个函数的多个参数拆分成多个函数调用,主要目的是避免平繁调用具有多个相同参数函数,又可以复用相同函数,具体可以用下面代码理解下

// 未柯里化之前
function sum(a, b, c) {
  return a + b + c;
}
sum(1, 2, 3);

函数柯里化后

function sum(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}
const a = sum(1);
const b = a(2);
const c = b(3);
console.log(c); // 6 or sum(1)(2)(3)

上面 🌰 好像还是不太明显,在具体业务中,你可能会写出这样的代码

// 根据正则规则校验某个字段
function regKey(reg, val) {
  return reg.test(val);
}
var isPhone = regKey(/^1[3,5,7,8,9]\d{9}$/, 13754123124);
const isNumber = regKey(/\d/, 'test');

改成函数柯里化后

function regKey(reg) {
  return (val) => {
    return reg.test(val);
  };
}
const checkPhone = regKey(/^1[3,5,7,8,9]\d{9}$/);
const checkNum = regKey(/\d/);

const isPhone = checkPhone(13754123124); // true
const isNumber = checkNum(123); // true

我们发现改完后,貌似柯里化后,代码反而变多了,但是代码的可读性以及拓展性比以前更友好,这点因特殊业务功能而定,也不是非要把用柯里化函数去处理所有的业务,具体因情况而定,这里只是举了个简单的例子。

回调函数在业务中使用的太多了,具体可以看下下面这个简单的例子,写一段为伪代码感受一下

const request = (params) => {
  const response = {
    code: 0,
    success: '成功',
    data: []
  };
  return (callback) => {
    callback(response);
  };
};
const queryList = () => {
  const getData = request({ pageIndex: 1, pageSize: 10 });
  getData((res) => {
    console.log(res); // {code: 0, success: '成功', data: []}
  });
};

这个就非常典型了,比如说一个循环里面

for (var i = 0; i < 10; i++) {
  (function () {
    var j = i;
    setTimeout(() => {
      console.log(j);
    }, i * 1000);
  })();
}

频繁触发事件,在指定一段时间内调用函数

// 模拟数据请求伪代码
const fetchList = () => {};
let flag = false;
window.addEventListener('scroll', () => {
  if (flag) {
    return;
  }
  flag = true;
  setTimeout(() => {
    flag = false;
    fetchList();
  }, 500);
});

利用定时器做缓冲器,当第二次调用时,清除上一次的定时器,在指定时间内重新调用函数

// 模拟数据请求伪代码
const fetchList = () => {};
const inputDom = document.getElementById('input');
let timer = null;
inputDom.oninput = function () {
  if (!timer) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fetchList();
    }, 500);
  }
};

以上案例都有用到闭包,闭包的身影无处不在,只是我们用的时候,我们并没有发现。

总结

最后,喜欢的点个赞,在看,转发,收藏等于学会,欢迎关注 Web 技术学苑,好好学习,天天向上!