深入理解JS闭包

深入理解JS闭包,第1张

在阅读这篇文章之前,我们先来做做两个思考题,有助于我们深入理解闭包,这也是我今天发现的比较有意思的题目,一起来看看吧:

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(); // 999
console.log(test);//10
console.log(n);//1
nAdd();
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(); // 999
console.log(test);//10
console.log(n);//1
nAdd();
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。闭包会在父函数外部改变父函数内部变量的值。如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要注意,不要随便改变父函数内部变量的值。当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,但是关联这段代码的作用域链不相同,用闭包概念来说,也就是产生了新的闭包

关于闭包的理解,到这里就结束了,希望对大家深入理解有帮助,要是有什么不理解也欢迎评论区留言,笔者会一一答复。大家也不要害怕闭包,闭包虽然难,可是闭包真的很重要,非常值得学习,虽然闭包在开发的时候用的很少,但是用到的时候几乎都是不可被别的方式替代的,但凡遇到永久 ,保护等关键字就是用闭包。 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/940148.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-17
下一篇 2022-05-17

发表评论

登录后才能评论

评论列表(0条)

保存