详解JavaScript闭包

1. 闭包是什么

闭包是什么:指的是一种允许函数访问并操作该函数外部变量的一种环境。就比如下面这种情况,内部函数f存在对外部函数fn的变量n的引用。

var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    f()
    return f
}

var x = fn()
x()
x()
console.log(n)
/* 输出
*  21
    22
    23
    10
/

2. 形成闭包的原因

形成闭包的原因

在ES5中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。

因此可以总结原因:外部函数虽然已经执行完毕,但内部函数仍然保留了对外部变量的引用,而这些变量并没有被垃圾回收机制释放。【当前作用域存在指向父级作用域的引用

这里扩展一下堆栈内存:

  • 栈内存:存储基本类型值
  • 栈内存的释放:一般当函数执行完后函数的私有作用域就会被释放,但也有特殊情况,如函数执行完,但函数的私有作用域内有内容被栈外的变量还在使用,栈内存就不会释放,里面的值也不会被释放;全局下的栈内存只有在页面关闭时才释放
  • 堆内存:存储引用类型的指针
  • 堆内存的释放:将引用类型的地址变量赋值为null,或没有变量引用这个地址,浏览器就被垃圾回收机制释放掉该地址。

3. 闭包的作用

闭包的作用

  • 保护函数的私有变量不受外加干扰,避免全局污染
  • 实现函数内的变量、属性私有化

4. 闭包的使用场景

闭包的使用场景【表现形式】:

  1. 返回一个函数,上面的代码已举例
  2. 作为函数参数传递【和1统称为高阶函数】
var a = 0
function fn() {
    var a = 1
    function fn1() {
        console.log(a)
    }
    return fn1
}
function fn2(params) {
    var a = 2
    params()
}
fn2(fn())  // 1  fn1函数使用fn的变量a形成闭包
  1. 只要使用了回调函数,实际上就是在使用闭包,如定时器、事件监听、Ajax请求等,回调函数保存了当前作用域以及window的作用域。
  2. IIFE立即执行函数创建闭包,保存了当前作用域以及window作用域。
var a = 0;
(function() {
    console.log(a)
})()
  1. 防抖,节流
// 防抖:多次操作只触发最后一次,如输入框响应式搜索
function debounce(fn, time) {
    let timer
    return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arguments)
        }, time);
    }
}
// 节流:多次操作只触发第一次,如点击登录按钮、获取验证码按钮
function throttle(fn, time) {
    let timer
    return function () {
        if (timer) return
        timer = setTimeout(() => {
            fn.apply(this, arguments)
            timer = null
        }, time);
    }
}
// 使用方法示例:debounce(fn(2, 3), 1000)
  1. 柯里化:接受多个参数的函数转化为一系列嵌套的单一参数函数。
function add(x) {
  return function(y) {
    return x + y;
  };
}

const curriedAdd = add(2);
console.log(curriedAdd(3)); // 5

5. 闭包可能存在的问题

闭包可能存在的问题

  1. 频繁使用闭包,大量变量无法被垃圾回收机制回收从而导致内存消耗过大【内存泄漏】

如何避免内存泄漏问题?

  1. 定时器、事件监听等在不需要的时候关闭,如vue中在生命周期beforeDestroy清除定时器、移除事件监听。
window.removeEventListener('scroll', this.handleScroll);
clearInterval(this.timer);
  1. 数据使用结束置为null,如vue中beforeDestroy周期时
beforeDestroy() {
    // 在组件销毁之前清理数据
    this.data = null; // 将数据设置为null,使其在垃圾回收时可以被释放
},

6. 经典面试题

经典面试题

  1. var变量循环输出问题:为什么输出全是5 如何让他输出1 2 3 4 5
for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)  //  5 5 5 5 5
  }, 0)
}
  • 方法一:立即执行函数实现闭包
for(var i = 1; i <= 5; i ++){
  (function (j) {
      setTimeout(function timer(){
        console.log(j)  //  1 2 3 4 5
      }, 0)
  })(i)
}
  • 方法二:使用letlet 具有块级作用域,形成的5个私有作用域都是互不干扰的。
for(let i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)  //  1 2 3 4 5
  }, 0)
}
  • 方法三:给定时器传入第三个参数,定时器会将第三个以及后面的参数都作为第一个参数的传的函数的参数。
for(var i = 1; i <= 5; i ++){
  setTimeout(function(i){
    console.log(i)  //  1 2 3 4 5
  }, 0, i)
}
  1. 字节
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(1);
result[0]();
result[1]();
result[2]();

此题的输出结果为:3 6 9,因为foo形成闭包,total被外层引用没有被销毁。

参考博客:

  1. # (建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上)
  2. # JS 闭包经典使用场景和含闭包必刷题
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值