闭包到底是啥?

闭包也是前端面试中常问的,那么闭包到底是啥?

闭包是什么

闭包的本质是一个函数,一个什么函数呢?一个使用了其包含函数变量的函数。所以更准确的说法是,闭包是一个函数+捕获的变量。如下:

javascript 复制代码
function init() {
  var name = "Mozilla"; // name 是 init 创建的局部变量
  function displayName() {
    // displayName() 是内部函数,它创建了一个闭包
    console.log(name); // 使用在父函数中声明的变量
  }
  displayName();
}
init();

可以看到displayName就是我们所说的使用了其包含函数init的变量name的函数,所以它是一个闭包。这个闭包实际上是因为JS的这种可嵌套函数的写法产生的,在其他如Java,C++等语言中是没有的。

闭包有什么作用?

  1. 模拟私有变量和方法
javascript 复制代码
function Stock(name, position) {
    this.name = name;
    this.addPosition = addPosition;
    this.minusPosition = minusPosition;
    this.getPosition = getPosition;
    
    let _position = position; // 持仓

    function add(count) {
        _position += count;
    }
    function addPosition(count) {
        add(count)
    }
    function minusPosition(count) {
        add(-count)
    }
    function getPosition() {
        return _position
    }
    
}
const s = new Stock('贵州茅台', 50000)

s.name // '贵州茅台'
s._positon // undefined
s.getPosition() // 50000
s.add() // 报错:Uncaught TypeError: s.add is not a function

s.addPosition(5000)
s.getPosition() // 55000
s.minusPosition(500)
s.getPosition() // 54500

以上代码我们模拟了一个私有变量_position和add,一个是股票的持仓,一个是内部的加法。虽然 Stock 构造函数没有返回内部函数,但它的实例方法(如 getPosition)仍然形成了闭包,因为它们访问了构造函数作用域中的 _position。

  1. 事件回调中的索引问题

这是一个在没有let和const时代非常容易犯的错。

javascript 复制代码
function bindEvent() {
    const btns = ['btn1', 'btn2', 'btn3'];
    const binds = [];

    for(var i = 0; i < btns.length; i++){
        binds[i] = function() {
            setTimeout(() => {
                console.log(`执行${btns[i]}的绑定事件:` + i)
            })
        }
    }
    return binds;
}
const binds = bindEvent()

binds[0]() // 执行undefined的绑定事件:3
binds[1]() // 执行undefined的绑定事件:3
binds[2]() // 执行undefined的绑定事件:3

我们本意是要给每个按钮绑定对于索引的输出语句,结果三个输出一样,且都没有达到要求。这是为什么呢?要解释这个问题,我们得知道 JS 的变量作用域,由于var所定义的变量不存在块中,而是存在于函数作用域中,所以,for循环中的 i 实际上是相当于在函数开始定义的,那么当整个bindEvent执行完毕之后,i 已经被+到了3。而我们在setTimeout回调中又使用了i,所以 i 不会消失,会等到binds中的函数执行时再使用,这个时候 i 的值已经时3了,所以输出就变成了实例中的那样。btns[3]是undefined,i 是3。

怎么解决这个问题呢?我们需要一个区别于bingEvent的作用域,将i的值保留下来。有两种做法,一种是在回调函数的外层套一个立即执行函数,锁定 i 的值;一种是使用let或者const来产生一个块作用域来锁定 i 的值。

javascript 复制代码
function bindEvent() {
    const btns = ['btn1', 'btn2', 'btn3'];
    const binds = [];

    for(var i = 0; i < btns.length; i++){
        binds[i] = (function(index) {
            return () => {
                setTimeout(() => {
                    console.log(`执行${btns[index]}的绑定事件:` +index)
                })
            }
        })(i)
    }
    return binds;
}
const binds = bindEvent()

binds[0]() // 执行btn1的绑定事件:0
binds[1]() // 执行btn2的绑定事件:1
binds[2]() // 执行btn3的绑定事件:2

function bindEvent() {
    const btns = ['btn1', 'btn2', 'btn3'];
    const binds = [];

    for(let i = 0; i < btns.length; i++){
        binds[i] = function() {
           setTimeout(() => {
               console.log(`执行${btns[i]}的绑定事件:` +i)
           })
        }
    }
    return binds;
}
const binds = bindEvent()
  1. 节流和防抖中使用
javascript 复制代码
// 防抖
function debounce(func, delay) {
    let timer = null;
    return function(){
        clearTimeout(timer);
        timer = setTimeout(()=> func(), delay);
    }
}

const bind = debounce(() => {
    console.log('执行一个请求操作')
}, 300)

for(let i = 0; i < 10; i++) {
    bind()
}

// 只输出一次:执行一个请求操作

我们在上面创建了一个防抖函数,我们连续调用10次,结果只输出了1次,因为每一次的执行我们都延后了300ms执行,这样的话,for循环中执行间隙小于300ms,所以10次只有一次执行成功了,这就是防抖。

javascript 复制代码
// 节流
function throttle(func, delay) {
  let lastTime = 0;
  return function(){
      const now = Date.now();
      if (now - lastTime > delay) {
          func.apply(this);
          lastTime = now;
      }
  }
}

let count = 1
const operate = throttle(() => console.log('执行一个连续操作' + count++), 100);
for(let i = 0; i < 10000000; i++) {
    operate()
} 

/* 输出: 
执行一个连续操作1
执行一个连续操作2
执行一个连续操作3
执行一个连续操作4
*/

如上我们将节流代码执行了1千万次,但是真正执行的只有4次,这就是节流的意义,否则控制台就会真的打印1千万次,导致页面挂掉。

闭包的作用域链

之所以闭包能访问外部函数的变量,是因为闭包在调用的时候,会将包含他的函数的变量对象引用保存起来,然后逐级保存包含外部函数的函数的变量对象,知道全局变量对象。这样,只要这个闭包存在,所有的外层作用域都不会被销毁,这就是作用域链,因此闭包也比普通函数多了一些内存消耗。

其实,你每天都在用闭包:React 的 Hooks(useState)、Vue 的响应式系统、模块化代码......底层都依赖闭包来维持状态。

相关推荐
2501_944448002 小时前
Flutter for OpenHarmony衣橱管家App实战:支持我们功能实现
android·javascript·flutter
会跑的葫芦怪8 小时前
若依Vue 项目多子路径配置
前端·javascript·vue.js
xiaoqi9229 小时前
React Native鸿蒙跨平台如何进行狗狗领养中心,实现基于唯一标识的事件透传方式是移动端列表开发的通用规范
javascript·react native·react.js·ecmascript·harmonyos
jin1233229 小时前
React Native鸿蒙跨平台剧本杀组队消息与快捷入口组件,包含消息列表展示、快捷入口管理、快捷操作触发和消息详情预览四大核心功能
javascript·react native·react.js·ecmascript·harmonyos
烬头882111 小时前
React Native鸿蒙跨平台实现二维码联系人APP(QRCodeContactApp)
javascript·react native·react.js·ecmascript·harmonyos
pas13611 小时前
40-mini-vue 实现三种联合类型
前端·javascript·vue.js
2601_9498333911 小时前
flutter_for_openharmony口腔护理app实战+预约管理实现
android·javascript·flutter
军军君0112 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three
xiaoqi92213 小时前
React Native鸿蒙跨平台如何实现分类页面组件通过searchQuery状态变量管理搜索输入,实现了分类的实时过滤功能
javascript·react native·react.js·ecmascript·harmonyos
qq_1777673713 小时前
React Native鸿蒙跨平台实现应用介绍页,实现了应用信息卡片展示、特色功能网格布局、权限/联系信息陈列、评分展示、模态框详情交互等通用场景
javascript·react native·react.js·ecmascript·交互·harmonyos