javascript中的闭包学习教程

一、简要介绍

其实关于闭包各个论坛社区里都有很多的文章来讲它,毕竟闭包是JavaScript中一个特色,也正因为这个雨中不同的特色也让闭包理解起来有一些吃力。笔者在这里不仅仅是想介绍闭包,也向列举一些笔者所见过的一些闭包。

闭包可谓是js中的一大特色了,即使你对闭包没概念,你可能已经在不知不觉中使用到了闭包。闭包是什么,闭包就是一个函数可以访问到另一个函数的变量。这就是闭包,解释起来就这么一句话,不明白?我们来看一个简单的例子:

function create()
{
    var i=0;

    return function()
    {
        i++;
        console.log(i);
    }
}
var c = create(); // c是一个函数
c(); // 函数执行
c(); // 再次执行
c(); // 第三次执行

在上面的例子中,create()返回的是一个函数,我们暂且称之为函数A吧。在函数A中,有两条语句,一条是变量i自增(i++),一条是输出语句(console.log)。第一次执行执行c()时会产生什么样的结果?嗯,输出自增后的变量i,也就是输出1;那么第二次执行c()呢,对,会输出2;第三次执行c()时会输出3,依次累加。这个create()函数依然满足了我们在刚开始时的定义,函数A使用到了另一个函数create()中的变量i。

可是为什么会产生这样的输出呢,为什么i就能一直自增呢,create函数已经执行完并返回结果了呀,可是为什么还能接着使用i呢,而且i还能自增。这里我们先去理解另一个概念,即作用域,了解作用域之后,再回头去理解闭包就好理解了。

二、作用域

变量的作用域无非就是两种:全局变量和局部变量。Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

var abc = 9999;
function fn()
{
    console.log( abc );
}
fn(); // 9999

另一方面 ,在函数外部无法读取函数内的局部变量

var abc = 123;
function fn()
{
    var n = 123;
}
alert( n ); // error :  n is not defined

特别说明:在函数内部声明变量的时候,一定要使用 var 命令, 如上面的代码,如果不用 var 则 n 使用的是全局变量。

三、如何从外部读取局部变量?

出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数的内部,再定义一个函数。

function f1()
{
    var a = 999;

    function f2()
    {
        var b = 888; 
    
        console.log( a ); // 999
    }
}

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1()
{
    var a = 999;

    function f2()
    {
        var b = 888; 
    
        console.log( a ); // 999
    }
    return f2;
}

var result = f1();
result(); // 999

那有人说,这里多麻烦,直接用 f1 返回 a,不就可以了,这样的话,不就直接使用局部变量了吗?其实这里说的更准确些,这样的话,返回的已经不是局部变量了,而是一个值。比如:我们一开始的那个 代码,如果把 create 函数,返回i ,那不管调用多少次 create ,返回的值都是一个固定的。不会像现在每调用一次,值增加一次了。 

这里以文章开头说的那一段代码为例,说一下代码执行的流程,这里也能更好的去理解这一过程。程序是从上往下顺序执行,遇到函数,直接跳过,来到 var c = create();这里会去调用 create 函数,按正常的情况下,如果 create 运行完,此函数就会从内存中删除,但是这次一不样,因为声明的变量 c ,还在使用着 create 的内部匿名函数,所以 create 并没有从内存删除,这时,变量 c 指向的是一个匿名函数地址,下面在调用 c 的时候,都是在执行这个匿名函数。而 create 中的变量 i 对于这个匿名函数而言,就是一个全局变量,所以,每调用一次,都是操作一下这个全局变量。所以 开头那段代码,和下面这段效果是一样

var i = 0;
function create()
{
    i ++;
    console.log( i );
}
create(); // 1
create(); // 2
create(); // 3

四、闭包的概念

上一节代码中的f2函数,就是闭包。各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

五、闭包的使用

1、在内存中维持一个变量。比如前面讲的小例子,由于闭包,函数create()中的变量i会一直存在于内存中,因此每次执行c(),都会给变量i加1.

2、保护函数内的变量安全。还是那个小例子,函数create()中的变量c只有内部的函数才能访问,而无法通过其他途径访问到,因此保护了变量c的安全。

3、实现面向对象中的对象。javascript并没有提供类这样的机制,但是我们可以通过闭包来模拟出类的机制,不同的对象实例拥有独立的成员和状态。

这里我们再看一个常用的实例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

<script type="text/javascript">
function libindclick()
{
    var liobj = document.getElementsByTagName('li');

    for(var i = 0; i < liobj.length; i ++)
    {
        liobj[i].onclick = function()
        {
            alert( i );
        }
    }
}
libindclick();
</script>
</body>
</html>

这里,我们的想法是给每一个li添加一个click事件,当点击的时候,弹出当前个数。可以运行之后,发现不管点击哪个,弹出的总是 5,  这是为什么呢?其实,我们在写 onclick 的时候,已经使用了闭包。

我们按第三部分,分析代码流程的方式,来分析一下这段代码,function() { alert(i); } 是一个匿名函数,也是一个闭包,也属于 libindclick 函数。所以在运行完 libindclick 函数之后,libindclick 并没有从内存消失,因为 li 的 click 正在使用着这个匿名函数。所以,当下次,点击 li 的时候,也就会调用这个匿名函数,而这个匿名函数中使用的变量 i 是 全局变量(也是 libindclick 的局部变量),这个全局变量i是在运行 libindclick 的时候生成的。

所以上面的过程是先执行的 libindclick 函数, 然后运行 for 循环,生成i, 结束libindclick函数,此时 i 为 5。  因为这个 click 是在 libindclick 函数结束后,才去调用的,所以,这时候,click用的值肯定是5了,如果还不明白,我们可以把这一段代码想象放慢运行的过程,比如:for 循环一次用了 10秒,当第一次循环完,即给 第一个 li 绑定了事件,这时,我们去点击的li,肯定是弹的 0,这个您可以通过写一个 sleep 函数,测试一下。

上面的代码还是和我们想要的有差别的,我们应该怎么来修改呢?办法也是很简单的,只要给每一个匿名函数传一个参数就可以了,修改后的如下

for(var i = 0; i < liobj.length; i ++)
{
    liobj[i].onclick = (function(i)
    {
        return function()
        {
            alert( i );
        }
    })(i);
}

再分析一下这段代码,现在 onclick 函数是一个立即支行的匿名函数的返回值,即 (function(i) { return function() { alert(i); } })(i); 我们把这个函数叫“匿名函数A”,这个函数,每运行一次,都会把当前的i做为参数传过去,而此时这个“匿名函数A”,有一个局部变量 i, 这个i 就是每次运行的i, 而又返回一个匿名函数 return function(i) { alert(i); }, 我们把这个函数叫“匿名函数B”, 匿名函数B就会当作 click 事件函数,而每次click后,就会调用“匿名函数B”,“匿名函数B”中的i是全局变量,向上一链找,即到“匿名函数A”中找,所以这时的i都不一样。

六、使用闭包的注意点

1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

本文参考文章:

http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

https://segmentfault.com/a/1190000002805295

未经允许不得转载:易读小屋  »  javascript中的闭包学习教程