前端开发必须知道的JS之闭包及应用

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

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

  作用域、作用域链、调用对象
  函数作用域分为词法作用域和动态作用域。
  词法作用域是函数定义时的作用域,即静态作用域。当一个函数定义时,他的词法作用域就确定了,词法作用域说明的是在函数结构的嵌套关系下,函数作用的范围。这个时候也就形成了该函数的作用域链。作用域链就是把这些具有嵌套层级关系的作用域串联起来。函数的内部[[scope]]属性指向了该作用域链。
  动态作用域是函数调用执行时的作用域。当一个函数被调用时,首先将函数内部[[scope]]属性指向了函数的作用域链,然后会创建一个调用对象,并用该调用对象记录函数参数和函数的局部变量,将其置于作用域链顶部。动态作用域就是通过把该调用对象加到作用域链的顶部来创建的,此时的[[scope]]除了具有定义时的作用域链,还具有了调用时创建的调用对象。换句话说,执行环境下的作用域等于该函数定义时就确定的作用域链加上该函数刚刚创建的调用对象,从而也形成了新的作用域链。所以说是动态的作用域,并且作用域链也随之发生了变化。再看这里的作用域,其实是一个对象链,这些对象就是函数调用时创建的调用对象,以及他上面一层层的调用对象直到最上层的全局对象。 
  譬如全局环境下的函数A内嵌套了一个函数B,则该函数B的作用域链就是:函数B的作用域—>函数A的作用域—>全局window的作用域。当函数B调用时,寻找某标识符,会按函数B的作用域—>函数A的作用域—>全局window的作用域去寻找,实际上是按函数B的调用对象—>函数A的调用对象—>全局对象这个顺序去寻找的。也就是说当函数调用时,函数的作用域链实际上是调用对象链。

  闭包
  在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据(看完下面的应用就会很好的体会这句话)。闭包的定义:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
  闭包就是嵌套在函数里面的内部函数,并且该内部函数可以访问外部函数中声明的所有局部变量、参数和其他内部函数。当该内部函数在外部函数外被调用,就生成了闭包。(实际上任何函数都是全局作用域的内部函数,都能访问全局变量,所以都是window的闭包)
  譬如下面这个例子:


代码如下:

<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种方式实现闭包,一种是用参数存储数组的值,一种是用临时变量存储,后者必须要深拷贝。所有要通过闭包存储非持久型变量,均可以用临时变量或参数两种方式实现。

";
}
})(), i * 500);
//or 写成下面这样也可以
/*
setTimeout((function(m) {
return function() {
$("proc").innerHTML += m + "
";
}
})(arr.join(",")), i * 500);
*/
}
return arr;
}
}
// -->

var a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];

Proc:

[Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]

应用2:
  这个是无忧上的例子(点击这里查看原帖),为每个<li>结点绑定click事件弹出循环的索引值。起初写成
id.onclick = function(){ alert(i); }  id.onclick = function(){alert(i);}
发现最终弹出的都是4,而不是想要的 1、2、3,因为循环完毕后i值变成了4。为了保存i的值,同样我们用闭包实现:

  • aa
  • aa
  • aa

[Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]

(ps:var a = (function(){})(); 与 var a =new function(){}效果是一样的,均表示自执行函数。)
  应用3:
  下面的code是缓存的应用,catchNameArr。在匿名函数的调用对象中保存catch的值,返回的对象由于被CachedBox变量引用导致匿名函数的调用对象不会被回收,从而保持了catch的值。可以通过CachedBox.getCatch("regionId");来操作,若找不到regionId则从后台取,catchNameArr 主要是为了防止缓存过大。


代码如下:

<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。  


代码如下:

<script type="text/javascript">
var GetId = (function() {
var id = 0;
return function() {
return id++;
}
})();
var newId1 = GetId();
var newId2 = GetId();
</script>

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

[Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]

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

var $ = function(id) { return "string" == typeof id ? document.getElementById(id) : id; }
var doConfirm = function(divId) {
$(divId).style.display = "";
function closeDiv() {
$(divId).style.display = "none";
}
return function(isOk) {
if (isOk) {
alert("Do deleting...");
}
closeDiv();
}
}

body
{
font-family: Arial;
font-size: 13px;
background-color: #FFFFFF;
}
#confirmDiv
{
width: 200px;
height: 100px;
border: dashed 1px black;
position: absolute;
left: 200px;
top: 150px;
}

你确定要删除吗?

[Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]

看了上面的这些应用,再回到前面的一句话:在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。这就是闭包的作用。也就说遇到需要存储动态变化的数据或将被回收的数据时,我们可以通过外面再包裹一层函数形成闭包来解决。
  当然,闭包会导致很多外部函数的调用对象不能释放,滥用闭包会使得内存泄露,所以在频繁生成闭包的情景下我们要估计下他带来的副作用。
  毕了。希望能对大家有所帮助。
者:JayChow
出处:http://ljchow.cnblogs.com

(0)

相关推荐

  • 一不小心就做错的JS闭包面试题

    由工作中演变而来的面试题 这是一个我工作当中的遇到的一个问题,似乎很有趣,就当做了一道题去面试,发现几乎没人能全部答对并说出原因,遂拿出来聊一聊吧. 先看题目代码: function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,? var b = fun(0).fun(

  • js闭包的用途详解

    我们来看看闭包的用途.事实上,通过使用闭包,我们可以做很多事情.比如模拟面向对象的代码风格:更优雅,更简洁的表达出代码:在某些方面提升代码的执行效率. 1 匿名自执行函数 我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处, 比如:别的函数可能误用这些变量:造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的). 除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一

  • JavaScript闭包函数访问外部变量的方法

    闭包是指有权访问另一个函数作用域中的变量的函数,但作用域的配置机制有一个需要注意的地方,即闭包只能取得包含函数中任何变量的最后一个值. 如以下案例: function create(){     var arr = new Array();        for (var i=0; i<10; i++){         arr[i] = function(){             return i;         };      }       return arr; }   var c

  • JavaScript闭包的简单应用

    闭包定义 在JavaScript中,当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包.简单说,闭包就是能够读取其他函数内部变量的函数. 闭包的作用: 1. 可以读取函数内部的变量 2. 让这些变量的值始终保持在内存中. 闭包简单应用 例一: function a() { var i = 0; function b() { console.log(++i); } return b; } var c = a(); //执行完var c=a()后,变量c指向了函数b,再执行c()后就会显示

  • 详谈JavaScript 匿名函数及闭包

    1.匿名函数 函数是JavaScript中最灵活的一种对象,这里只是讲解其匿名函数的用途.匿名函数:就是没有函数名的函数. 1.1 函数的定义,首先简单介绍一下函数的定义,大致可分为三种方式 第一种:这也是最常规的一种 复制代码 代码如下: function double(x){     return 2 * x;   } 第二种:这种方法使用了Function构造函数,把参数列表和函数体都作为字符串,很不方便,不建议使用. 复制代码 代码如下: var double = new Functio

  • javascript深入理解js闭包

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量. Js代码 var n=999; function f1(){ alert(n); } f1(); // 999 另一方面,在函数外部自然无法读取函数内的局部变量. Js代码 function f1(){ var n=999; } alert(n); // error 这里有一个地方需要注意,函数

  • js bind 函数 使用闭包保存执行上下文

    复制代码 代码如下: window.name = "the window object" function scopeTest() { return this.name; } // calling the function in global scope: scopeTest() // -> "the window object" var foo = { name: "the foo object!", otherScopeTest: fu

  • 一分钟理解js闭包

    什么是闭包? 先看一段代码: function a(){ var n = 0; function inc() { n++; console.log(n); } inc(); inc(); } a(); //控制台输出1,再输出2 简单吧.再来看一段代码: function a(){ var n = 0; this.inc = function () { n++; console.log(n); }; } var c = new a(); c.inc(); //控制台输出1 c.inc(); //

  • JavaScript闭包 懂不懂由你反正我是懂了

    越来越觉得国内没有教书育人的氛围,为了弄懂JS的闭包,我使出了我英语四级吃奶的劲去google上搜寻着有关闭包的解释,当我看到stackoverflow上这一篇解答,我脑中就出现了一句话:就是这货没跑了! 不才译文见下,见笑了. Peter Mortensen问: 就像老Albert所说的,"如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂."好吧,我试着向一个27岁的朋友就是JS闭包(JavaScript closure)却彻底失败了. 你们会怎么把它解释给一个充满好奇

  • JavaScript 匿名函数(anonymous function)与闭包(closure)

    引入 匿名函数 闭包 变量作用域 函数外部访问函数内部的局部变量 用闭包实现私有成员 引入 闭包是用匿名函数来实现.闭包就是一个受到保护的变量空间,由内嵌函数生成."保护变量"的思想在几乎所有的编程语言中都能看到. 先看下 JavaScript 作用域: JavaScript 具有函数级的作用域.这意味着,不能在函数外部访问定义在函数内部的变量. JavaScript 的作用域又是词法性质的(lexically scoped).这意味着,函数运行在定义它的作用域中,而不是在调用它的作用

随机推荐