朴实无华的 JavaScript 闭包

2020-10-17 15:52:15 蜻蜓队长

什么是闭包?

我想每一位前端开发都应该听说过闭包,每当面试官问谈谈你对闭包的理解:通常我都是回复当函数A内部嵌套了函数B,那么我们可以把B函数称为A函数的闭包。当然这么说并没有错,这是闭包的表现我们是否有想过闭包的本质是什么?闭包的场景是什么?

带着以上的几个问题我们来对闭包一探究竟。

一切得从词法作用域开始说起

来看一个MDN上的例子:

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

我们分析下函数运行时作用域是如何调起的:

  1. 当运行init()的时候,在函数内部定义了一个局部变量name,声明了一个函数displayName。这时候js会调起一个init()函数的作用域。
  2. 当运行displayName()的时候,js会调起displayName的作用域,当执行到alert(name)由于当前函数作用域中并没有找到变量name于是作用域环境就向外部执行环境函数init中查找这个查找过程就形成了作用域链(ps:如果在函数的外部依然没有查找到那么执行环境会向全局执行环境中查找,如果找不到通常会发生一些错误),找到name后停止查询执行alert

其实在inint执行环境的外部还存在一个全局环境也就是window

根据以上的例子我们知道了函数运行时会调起当前作用域试想一下如果在displayName的内部定义了变量name那么alert的到底是哪个name,这里应该能猜到当然是内部的name,因为在作用域中查找是需要时间的。能在当前作用域中找到就会屏蔽掉外部作用域中的同名变量这也符合一般规律。

不得不提的JavaScript内存管理

理解了作用域的概念之后我们还要了解JavaScript中内存的管理。 这里有两个概念一个是分配内存另一个就是回收内存

内存分配

为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。

内存回收

JavaScript 同样嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

那么闭包内存回收之间有什么联系呢? 废话不多说直接开始看代码:

function a(){
    var n = 0
    var add = function(){
        return n += 1
    }
    return add
}
var c = a()
c() // n = 1
c() // n = 2
c() // n = 3
.....
复制代码

可以看到函数 a 返回一个名为 add的函数 并在 add 的内部引用了函数 a 中定义的变量 n,当我们把 a 函数的运行结果保存在变量 c 中第一次运行c()返回 1 第二次运行c()返回 2 以此累加。 函数每次返回的 n 都是在上次运算的基础上进行。

当程序执行到代码var c = a()的时候:

  1. 调用a() 返回 add 函数的引用。
  2. 在全局作用域中定义了变量c保存了add函数的引用。

当程序执行到代码c()的时候:

  1. add 函数被执行,n += 1在当前函数的中试着寻找变量n,显然是没有的,于是执行环境只能向父作用域去找找看了。
  2. 然后js就沿着作用域链a函数中继续搜索变量n
  3. 终于在a函数中发现定义了变量n = 0找到变量n并对其进行 +1 计算并返回 n += 1 的结果

变量c引用了add函数而add函数引用了变量n,那么变量c间接地通过了add函数完成了对变量n引用。所以就导致了js引擎一直没有对变量n进行内存回收,因此我们每次运行c()都会基与内存中变量n的值,故而产生了闭包这种现象,看到这里是不是有种恍然大悟的感觉。

学习编程往往都是由一个简单的示例到复杂的案例,看了闭包的外形,且掌握了闭包的本质,那么接下来我想用几个实际开发中碰到的问题用闭包的方式去解决。

闭包的应用

购物车商品计数器

需求:

  1. 可以对商品数量进行增加、减少、访问。
  2. 一个页面下可以存在多个计数器。
  3. 多个计数器分别只对当前商品起作用,即多个计数器之间状态互不干扰。
  4. 除了计数器本身提供的方法之外,无法通过别的手段访问和修改计数器中的状态。
function counter() {
  var num = 0;
  let obj = {
    add: function add() {
      return (num += 1);
    },
    reduce: function add() {
      return (num -= 1);
    },
    get: function() {
      return num
    }
  };
  return obj;
}
let c1 = counter()
c1.add()  
let c2 = counter()
复制代码

用闭包实现 防抖&&节流

防抖(debounce)

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

function debounce(fn,wait){
    let timer
    return function(){
        if(timer){
            clearTimeout(timer)
        }
        timer = setTimeout(()=>{
            fn.apply(this,arguments)
        },wait)
    }
}

function a(a){
    console.log('a:',a)
}

var b = debounce(a,1000) // 在一秒钟之内只能触发一次,如果再次触发则重新计时

window.addEventListener('scroll',function(){b('hello')})
复制代码

防抖的应用场景

  • 输入框联想查询
  • 阻止按钮在规定时间内被多次点击
  • 浏览器窗口大小调整

节流

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

function throttling(fn,wait){
    let oldTime = 0
    return function (){
        let nowTime = Date.now()
        if(nowTime-oldTime > wait){
            oldTime = Date.now()
            fn.apply(this,arguments)
        }
    }
}

function a(a){
    console.log('a:', a)
}

var b = throttling(a,1000)

window.addEventListener('resize',function(){
    b('hello')
    
},false)
复制代码

节流的应用场景

  • 限制接口调用的频率
  • 对滚动到底部或者下拉刷新事件的次数做限制
  • 高频率的按钮点击等

后话

我们用了两个例子了解了JavaScript词法作用域内存管理。在从闭包外表,了解到闭包本质就是内部的作用域被外部引用导致了内存得不到释放于是产生了闭包。最后用闭包的方式完成了节流防抖函数。

以上就是我所理解的闭包,其中可能因为本人学艺不精导致内容不可避免地出现一些错误,希望大佬能帮忙指正。

如果这篇文章对您理解闭包有所帮助的话别忘了点

以上内容来自于网络,如有侵权联系即删除
相关文章

上一篇: DOM 事件模型或 DOM 事件机制

下一篇: 【开发经验】Flutter组件的事件传递与数据控制

在线咨询
客户经理