在阅读这篇文章之前,我们先来做做两个思考题,有助于我们深入理解闭包,这也是我今天发现的比较有意思的题目,一起来看看吧:
var n = 1;function fun1() { test = 10; var n = 999; nAdd = function() { n += 1; console.log(n); } function fun2() { console.log(n); } return fun2; }var result = fun1();result(); // 999console.log(test);//10console.log(n);//1nAdd();result(); // 1000
由于在函数内部声明变量test和nAdd的时候没有使用var命令,在函数fun1执行的时候这两个变量便作为全局变量来声明
我见网上有博主说闭包就是嵌套函数中的内部函数,也有博主说闭包是嵌套函数的外部函数,从上面这个栗子来看,显然这都是不准确的说法,在这里官方给出的定义更为准确,我们先来看看官方给出的定义吧:
闭包是函数和声明该函数的词法环境的组合。
再来看看官方给出的示例:
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName;}var myFunc = makeFunc();myFunc();
很显然这里存在嵌套函数,并且嵌套的内部函数使用了外部函数的变量,于是产生了闭包,按照官方的定义来理解的话,这里的闭包指的就是从makeFunc函数的第一句到return语句这一句。
感觉看了上面这一堆还是稀里糊涂怎么办?来,我们再重新捋一捋。
一、理解闭包第一步,理解JS变量的作用域
要真正理解闭包,首先,我们必须理解Javascript特殊的变量作用域(具体可以参考我的上篇文章,深入理解JS作用域和作用域链),JavaScript的一个特殊之处就在于它的变量访问规则:
1.函数内部可以直接读取全局变量
var n=999; function f1(){ alert(n); } f1(); // 999
2.在函数外部无法读取函数内的局部变量
function f1(){ var n=999; } alert(n); // Uncaught ReferenceError: n is not defined
但是这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量! (补充:在函数内部声明全局变量被称为隐式声明)
function f1(){ n=999; } f1(); alert(n); // 999
3.内部函数可以访问外部函数的变量
function f1(){ var n=999; (function f2(){ alert(n); })(); } f1();//999
二、理解闭包第二步,如何实现从外部读取局部变量
在使用JS进行编程的时候,我们有时候需要在函数外部得到函数内的局部变量,可是我们前面已经说了,正常情况下这是不可能的。嘿嘿,既然都说了是正常情况,那肯定有“不正常情况”啦!那就是在函数的内部,再定义一个函数。
function fun1(){ var n=999; function fun2(){ alert(n); // 999 } } fun1();
在上面的代码中,函数fun2就被包括在函数fun1内部,这时fun1内部的所有局部变量,对fun2都是可见的。但是反过来就不行,fun2内部的局部变量,对fun1都是不可见的。这就是Javascript语言所特有的链式作用域结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。总结:父对象的所有变量,对子对象都是可见的,反之则不成立。
这样我们就能够访问到函数fun1内部的变量了,可是问题来了,我们无法从外部调用函数fun2,那我们应该怎样才能够在外部访问fun2呢,我们只需要将fun2作为fun1的返回值返回,这样我们不就能在fun1外部访问到fun1内部的变量了吗,如下:
function fun1(){ var n=999; function fun2(){ alert(n); } return fun2; } var result=fun1(); result(); // 999
三、理解闭包第三步,闭包的概念
关于闭包的概念,在前面我们已经说了官方给出的定义,官方给的定义虽然比较准确一点,但是对于初学者来说晦涩难懂,这里来说说我对闭包的理解,我的理解是,闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
前面我们虽然也说了闭包不是一个函数,但是为了方便理解和学习我们通常可以将闭包称为一个函数,但是我们也要时刻在心里记住闭包不是一个函数,而是函数和声明该函数的词法环境的组合,这个环境包含了这个闭包创建时所能访问的所有局部变量。记住,它是一个组合!组合!
四、理解闭包的第四步,闭包的用途
想要使用闭包,必须知道它的结构,也是它的产生条件:
一个函数,里面有一些变量和另一个函数
外部函数里面的函数使用了外部函数的变量
外部函数最后把它里面的那个函数用return抛出去
以及闭包的作用:
在函数外部可以读取函数内部的变量
让这些变量的值始终保持在内存中
现在我们再来分析分析文章开始给出的那个题目,为了方便观察,我在这里再插入那段代码:
var n = 1;function fun1() { test = 10; var n = 999; nAdd = function() { n += 1; console.log(n); } function fun2() { console.log(n); } return fun2; }var result = fun1();result(); // 999console.log(test);//10console.log(n);//1nAdd();result(); // 1000
在这段代码中,result实际上就是闭包fun2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了函数fun1中的局部变量n一直保存在内存中,并没有在fun1调用执行完之后被自动清除。
发生这样的情况原因就在于fun1是fun2的父函数,而fun2又通过fun1的return语句被赋给了一个全局变量,这导致fun2始终在内存中,而fun2的存在依赖于fun1,因此fun1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
另外需要注意的地方是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数,而这个匿名函数本身也是一个闭包,所以nAdd可以在函数外部对函数内部的局部变量进行操作。
使用闭包时需要注意:
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不需要的局部变量赋值为null。
闭包会在父函数外部改变父函数内部变量的值。如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要注意,不要随便改变父函数内部变量的值。
当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,但是关联这段代码的作用域链不相同,用闭包概念来说,也就是产生了新的闭包
关于闭包的理解,到这里就结束了,希望对大家深入理解有帮助,要是有什么不理解也欢迎评论区留言,笔者会一一答复。大家也不要害怕闭包,闭包虽然难,可是闭包真的很重要,非常值得学习,虽然闭包在开发的时候用的很少,但是用到的时候几乎都是不可被别的方式替代的,但凡遇到永久 ,保护等关键字就是用闭包。
原文:https://juejin.cn/post/7097141521102667813