夜间模式CodeSnippetStyle:
日间模式CodeSnippetStyle:

0%


#JavaScript 闭包问题详解 #打倒心魔

闭包

网络上很多资料,都有描述闭包,解释的又繁琐又复杂。 这里尝试用自己的理解去解释一下,什么是闭包。对我个人来讲,也是谈闭包色变,看了很多遍,每次都觉得自己看明白了,几天不见,又懵逼了。 而且每次看别人的文章都感觉理解起来很困难。 大家惯用的做法是将一个已经很抽象的问题,进一步抽象成一个看似好理解的模型,实际上真的很痛苦啊喂!! 所以在参考了众多相关问题后,我打算自己总结一下,下次忘记就可以很快的回溯了。

关于闭包的问题,我觉得,要先从广义上去理解,然后落地到代码实现上, 此外,比起什么是闭包这个概念,明白它有哪些用处,以及为什么需要用到它,才是最重要。 毕竟,往往不好理解的东西通常用着用着就理解了。

1. 到底,什么是闭包?

在JavaScript 中,有作用域链的概念, 总结的讲,其实就是,子作用域能够向上访问父作用域中的变量,反之则不行。

let str = 'hello world!';
function func(){
    let name='henrry'
    console.log(str);
}
func();//hello world!
console.log(name);// name is not defined

那么,有没有一种办法,能够让我们,能够从父级作用域去访问子作用域中的变量呢?我们把能够从父作用域成功访问到子作用域的这种情况,称之为 “闭包” 。

img

它其实,是一种间接访问子作用域中变量的方式。以下代码,是一个基本的实现:

function outer() {
    let str = "hello world!"
    function inner() {
        return str;
    }
    return inner;
}
let recOuter = outer();
console.log(recOuter());//hello world

代码说明: 在这段代码中, 有三个作用域我们一定要先明确。 第一个就是最外层的作用域(记作S1), 即outer 函数被定义的这一层; 第二个就是outer 函数体内,也是inner函数被定义的那一层(记作S2); 第三个就是inner 函数体内(记作S3)。 我们希望在 S1 访问到 S2 中的 str 这个变量, 如果直接访问是不被允许的, 因此,我们在S2所在层,创建 了一个新的函数 inner 这个函数什么事情也不做,只是如果被执行将会把 str 返回,紧接着,我们又返回了inner 这个定义好的函数,等待被触发。 现在,如果outer 函数触发,我们将会得到 inner 函数,如果我们再触发返回的inner 函数, 就会获取到str 的值。 在最外层S1, 我们创建了recOuter 变量用以接收 outer 函数的返回值,即 inner, 然后我们执行了inner 函数(recOuter()) , 输出了 "hello world"。

这个过程其实为了尽可能的详细,描述的有些繁琐。 其实,如果从代码上看,反而简单的多。 就是在outer 函数内部定义了一个函数专用于返回outer 函数体中定义的变量。 这整个过程其实就做了一件事,把outer 函数内的 str 返回到S1 作用域 ,(换句话说,就是:在S1作用域内访问到S2中的str变量)。 这就是闭包。

2. 其实你每天的代码都在闭包

为什么这么说呢?类似以下的代码,应该是每天都有写的吧?

function calcGap(a, b) {
    if (a > b) {
        return a - b;
    } else {
        return b - a;
    }
}

let num1 = 2;
let num2 = 9;

let result = calcGap(num1, num2)
console.log("the gap is:", result);//7

那么重新审视一下这段及其普通的代码, 如果不直观,稍作改动:

function calcGap(a, b) {
    if (a > b) {
        let gap = a - b;
        return gap;
    } else {
        let gap = b - a
        return gap;
    }
}

let num1 = 2;
let num2 = 9;

let result = calcGap(num1, num2)
console.log("the gap is:", result);

且不用管if...else作用域的问题,他们是块级作用域,就当作和calcGap 函数体在同一个作用域就好。

我们最终需要的,是这个gap值,而这个值定义在 calcGap 函数内部,我们在外部想要打印它,不就是在尝试“在外层作用域去访问子作用域中的变量"吗? 这不就是闭包吗。 这就是闭包!所以,其实我们每天都在写。

3. 利用闭包可以有哪些实现?

其实这个问题,远比 ”闭包究竟是什么“ 重要的多, 因为概念容易忘记, 通过不断的去实现,去实践,从能真正的反过来去理解”闭包“。 作为初学者,其实最困难的就是对它一知半解,也不是完全不知道,能说出个大概,可就是感觉哪里没理解透彻,正是这个原因。 缺少实践。

阮一峰对闭包的用处是这样描述的:

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

https://ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

这两个应用方向,是从本质上去描述的,它能做什么。

  • 读取子作用域变量
  • 让变量保持在内存中

关于第二点,有一个很好的示例,一个是个日常场景 —— 计数器

这个例子也更够帮助我们更进一步的理解闭包的特点。

实现1 : 计数器

假设我们需要执行一个函数,counter就加一, 如果不考虑闭包,一般会是这样:

let counter = 0;

function count() {
    console.log(counter++);
}
count();//0
count();//1
count();//2

这样虽然简单,但是也存在一些问题,或者说有值得优化的地方。 在我们的代码逻辑中,可能会有很多类似 counter 这样的变量,它没别的什么作用,只是维护了一个计数值。 如果逻辑代码很多的时候,这种类似的变量可能会有很多个,在同一个作用域中,如果有类似功能的变量,甚至还需要为同样功能的变量取不同的变量名,例如 counter1, counter2... 这显然,不够酷 。

那么,我们能不能让整个计数逻辑成为一个整体呢? 这时候就需要依靠闭包的实现了:

function count() {
    let counter = 0;
    console.log(counter++);
}
count();//0
count();//0
count();//0

我们首先希望,要是能把这个变量放在函数体内部,就不会和其他变量相互影响了。 但是直接放进去肯定是不行的,我们发现了问题,因为函数一执行,counter 就被重置为了0 , 这样以来就达不到计数的效果了。 怎么办 ? 要是能够保存这个counter 值的状态就好了! 且看:

function count() {
  let counter = 0;
  function closureFunc() {
    return counter++;
  }
  return closureFunc;
}
let closureF = count();

console.log(closureF());//0
console.log(closureF());//1
console.log(closureF());//2

这就是一个计数器的实现,最值得我们去刨析的是,它为什么会保存状态

let closureF = count() , closureF 变量 就接收到了count 函数的执行返回值 ——函数 closureFunc

因为在最外层,我们创建了 closureF 这一个变量,它会保持对函数 closureFunc 的引用, 因此只要变量closureF 没有被销毁, 函数closureFunc 也不会被销毁, 连带着, counter 也不会被销毁。它们将会始终保存在内存中, 所以就保存了状态。 这属于JavaScript 垃圾回收机制内容。

也正是因为这个原因,所以在调用的使用需要先定义一个变量closureF 保持对count() 返回值的引用,这样就返回值就不会被回收掉。就能实现保存状态的目的。 不要想当然地这样去调用:

console.log(count()());//0
console.log(count()());//0
console.log(count()());//0

这样做并不会保存状态,因为执行完毕就会被回收机制回收掉。

怎么主动断开引用,让它被回收呢? 在退出逻辑块的时候,将其置为null 以释放掉内存。

function count() {
  let counter = 0;
  function closureFunc() {
    return counter++;
  }
  return closureFunc;
}
let closureF = count();

console.log(closureF());//0
console.log(closureF());//1
console.log(closureF());//2
closureF = null;//释放当前的变量, 使得`closureFunc`被回收掉

这也引出了闭包的优点和缺点的讨论:

4. 闭包的优缺点

优点: 不污染变量,让逻辑代码更整体。

缺点: 如果闭包函数所定义的上下文中(作用域)存在着很多定义的变量,那么这些变量都会被持续保存状态,而不会触发JavaScript回收机制,白白占用内存,在ie中还有可能造成内存溢出。 所以在闭包的使用上,需要花费程序设计成本以尽量减少不必要的内存占用。

注:对于缺点的说明,“闭包函数所定义的上下文中(作用域)存在着很多定义的变量”, 这个在以上示例中,就值得是 let counter = 0; , 在实际开发中,可能这里有一堆变量。

5. 闭包的更多应用实现

相比较,什么是闭包,闭包有哪些实现才是最重要的。

关闭闭包的更过应用实现,见:https://www.bilibili.com/video/BV1z7411v7T1?p=2&spm_id_from=pageDriver

posted @ 2021-04-22 10:42  暮冬有八  阅读(82)  评论(0编辑  收藏  举报
BACK TO TOP

😀迷海无灯听船行。Github WeChat