前端开发必须知道的JS(二) 闭包及应用


  在前端开发必须知道的JS(一) 原型和继承一文中说过下面写篇闭包,加之最近越来越发现需要加强我的闭包应用能力,所以此文不能再拖了。本文讲的是函数闭包,不涉及对象闭包(如用with实现)。如果你觉得我说的有偏差,欢迎拍砖,欢迎指教。

 

  一. 闭包的理论

  首先必须了解以下几个概念:

 

  执行环境

  每调用一个函数时(执行函数时),系统会为该函数创建一个封闭的局部的运行环境,即该函数的执行环境。函数总是在自己的执行环境中执行,如读写局部变量、函数参数、运行内部逻辑。创建执行环境的过程包含了创建函数的作用域,函数也是在自己的作用域下执行的。从另一个角度说,每个函数执行环境都有一个作用域链,子函数的作用域链包括它的父函数的作用域链。关于作用域、作用域链请看下面。

 

  作用域、作用域链、调用对象

  函数作用域分为词法作用域和动态作用域。

  词法作用域是函数定义时的作用域,即静态作用域。当一个函数定义时,他的词法作用域就确定了,词法作用域说明的是在函数结构的嵌套关系下,函数作用的范围。这个时候也就形成了该函数的作用域链。作用域链就是把这些具有嵌套层级关系的作用域串联起来。函数的内部[[scope]]属性指向了该作用域链。

  动态作用域是函数调用执行时的作用域。当一个函数被调用时,首先将函数内部[[scope]]属性指向了函数的作用域链,然后会创建一个调用对象,并用该调用对象记录函数参数和函数的局部变量,将其置于作用域链顶部。动态作用域就是通过把该调用对象加到作用域链的顶部来创建的,此时的[[scope]]除了具有定义时的作用域链,还具有了调用时创建的调用对象。换句话说,执行环境下的作用域等于该函数定义时就确定的作用域链加上该函数刚刚创建的调用对象,从而也形成了新的作用域链。所以说是动态的作用域,并且作用域链也随之发生了变化。再看这里的作用域,其实是一个对象链,这些对象就是函数调用时创建的调用对象,以及他上面一层层的调用对象直到最上层的全局对象。 

  譬如全局环境下的函数A内嵌套了一个函数B,则该函数B的作用域链就是:函数B的作用域—>函数A的作用域—>全局window的作用域。当函数B调用时,寻找某标识符,会按函数B的作用域—>函数A的作用域—>全局window的作用域去寻找,实际上是按函数B的调用对象—>函数A的调用对象—>全局对象这个顺序去寻找的。也就是说当函数调用时,函数的作用域链实际上是调用对象链。

 

  闭包

  在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据(看完下面的应用就会很好的体会这句话)。闭包的定义:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

  闭包就是嵌套在函数里面的内部函数,并且该内部函数可以访问外部函数中声明的所有局部变量、参数和其他内部函数。当该内部函数在外部函数外被调用,就生成了闭包。(实际上任何函数都是全局作用域的内部函数,都能访问全局变量,所以都是window的闭包)

  譬如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type= "text/javascript" >
     function  f(x) {
         var  a = 0;
         a++;
         x++;
         var  inner = function () {
             return  a + x;
         }
         return  inner;
     }
     var  test = f(1);
     alert(test());
</script>

  垃圾回收机制:如果某个对象不再被引用,该对象将被回收。  

  再结合前面所讲的一些概念,在执行var test=f(1)时创建了f的调用对象,这里暂且记作obj,执行完后虽然退出了外部执行环境,但内部函数inner被外部函数f外面的一个变量test引用。由于外部函数创建的调用对象obj有一个属性指向此内部函数,而现在这个内部函数又被引用,所以调用对象obj会继续存在,不会被垃圾回收器回收,其函数参数x和局部变量a都会在这个调用对象中得以维持。虽然调用对象不能被直接访问,但是该调用对象已成为内部函数作用域链中的一部分,可以被内部函数访问并修改,所以执行test()时,可以正确访问x和a。所以说, 当执行了外部函数时,生成了闭包,被引用的外部函数的变量将继续存在。

 

  二. 闭包的应用

  应用1:

  这个是我在用js模拟排序算法过程遇到的问题。我要输出每一次插入排序后的数组,如果在循环中写成

  setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500);

会发现每次输出的都是最终排好序的数组,因为arr数组不会为你保留每次排序的状态值。为了保存会不断发生变化的数组值,我们用外面包裹一层函数来实现闭包,用闭包存储这个动态数据。下面用了2种方式实现闭包,一种是用参数存储数组的值,一种是用临时变量存储,后者必须要深拷贝。所有要通过闭包存储非持久型变量,均可以用临时变量或参数两种方式实现。


 

  应用2:

  这个是无忧上的例子(点击这里查看原帖),为每个<li>结点绑定click事件弹出循环的索引值。起初写成

id.onclick = function(){
      alert(i);
   }

  id.onclick = function(){alert(i);}

发现最终弹出的都是4,而不是想要的 1、2、3,因为循环完毕后i值变成了4。为了保存i的值,同样我们用闭包实现:


  (ps:var a = (function(){})(); 与 var a =new function(){}效果是一样的,均表示自执行函数。)

 

  应用3:

  下面的code是缓存的应用,catchNameArr。在匿名函数的调用对象中保存catch的值,返回的对象由于被CachedBox变量引用导致匿名函数的调用对象不会被回收,从而保持了catch的值。可以通过CachedBox.getCatch("regionId");来操作,若找不到regionId则从后台取,catchNameArr 主要是为了防止缓存过大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script type= "text/javascript" >
     var  CachedBox = ( function () {
         var  cache = {}, catchNameArr = [], catchMax = 10000;
         return  {
             getCatch: function (name) {
                 if  (name in  cache) {
                     return  cache[name];
                 }
                 var  value = GetDataFromBackend();
                 cache[name] = value;
                 catchNameArr.push(name);
                 this .clearOldCatch();
                 return  value;
             },
             clearOldCatch: function () {
                 if  (catchNameArr.length > catchMax) {
                     delete  cache[catchNameArr.shift()];
                 }
             }
         };
     })();
</script>

  同理,也可以用这种思想实现自增长的ID。  

1
2
3
4
5
6
7
8
9
10
<script type= "text/javascript" >
     var  GetId = ( function () {
         var  id = 0;
         return  function () {
             return  id++;
         }
     })();
     var  newId1 = GetId();
     var  newId2 = GetId();
</script>

 

  应用4:

  这个是无忧上月MM的例子(点击这里查看原帖),用闭包实现程序的暂停执行功能,还蛮创意的


 

  把这个作用延伸下,我想到了用他来实现window.confirm。


 

  看了上面的这些应用,再回到前面的一句话:在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。这就是闭包的作用。也就说遇到需要存储动态变化的数据或将被回收的数据时,我们可以通过外面再包裹一层函数形成闭包来解决。

  当然,闭包会导致很多外部函数的调用对象不能释放,滥用闭包会使得内存泄露,所以在频繁生成闭包的情景下我们要估计下他带来的副作用。

 

  毕了。希望能对大家有所帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作为一个大型语言模型,我的职责是基于对大量样本数据的训练和学习,为用户提供一系列任务解决方法和自然语言生成能力。虽然我对 JavaScript 有一些了解,但是相比一个专业的前端开发工程师,我的知识还比较有限。不过,我还是可以大致介绍一下 JavaScript 的基本特点和应用。 JavaScript 是一种脚本语言,通常被运行在浏览器端来增强网站的交互性。它是一种动态类型、解释性、具有面向对象和函数式编程能力的高级语言,可以用于实现网页动态效果、表单验证、网络请求、数据可视化、跨平台移动应用等各种应用。JavaScript 最显著的特点是它可以在客户端进行实时交互,而无需刷新页面,因此成为了现代 Web 应用的重要组成部分。 在 JavaScript 中,可以使用变量、运算符、控制结构等基本语法进行编程。同时,JavaScript 还有一些特殊的数据类型和语法,如对象、数组、函数、闭包等,可以帮助开发者更加高效地完成各种任务。 JavaScript 的优势在于其高度灵活性和开放性,开发者可以基于各种框架、库和 API 进行开发,从而实现更多更复杂的功能。同时,JavaScript 社区也非常活跃,信息非常丰富,这为学习和运用提供了很大的便利。 当然,JavaScript 也有着一些不足之处,例如程序的安全性、性能问题、兼容性等,在实际开发中需要加以注意和解决。但我相信,在前端开发的道路上,JavaScript 仍然是一个不可或缺的重要技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值