您的位置:首页 - 教程 - JavaScript - 正文
JavaScript葵花宝典之闭包

闭包,写过JS脚本的人对这个词一定不陌生,都说闭包是JS中最奇幻的一个知识点,  虽然在工作中,项目里经常都会用到~  但是是不是你已经真正的对它足够的了解~~

又或者是你代码中出现的闭包,并不是你刻意而为之的行为~ 又或者是因为能达到效果,也知道是闭包,但是原理却不知道?。。。。

一千个人就有一千个哈姆雷特~  每个人也许都有自己对闭包的理解, 我也不例外, 曾经N次百度过闭包,却没有真正的消化过这个知识点, 也曾工作中无数次运用过闭包, 却不知其所以然~~

 

所以,我想把我理解的闭包,自己总结一下,虽然很多都是自己的理解, 但是总结再更正,才会越来越好~~

 

首先是闭包的定义:

  • 维基百科对闭包的定义:
闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
  • 书籍《你不知道的Javascript》对闭包的定义: (个人觉得对闭包阐释最好的一本书)
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数实在当前词法作用域之外执行。

 

上面的概念基本上已经说明了闭包是什么,这只是一个概括,不知道的人看了之后还是不会知道,所以要知道闭包还是需要庖丁解牛一样的一层一层的去理解~~

 

给自己几个疑问? 然后对这些疑问一层一层的理解,直到理解为止!

  • 闭包产生的环境?
  • 闭包的作用?
  • 闭包的一些使用场景?
  • 闭包的弊端?

 

闭包的环境:

既然是叫做闭包,里面有个包字,那么肯定就是被包围的意思咯~ 竟然是被包围,肯定是在一个空间中,而JS能代表空间的是什么? 就是作用域,作用域又有全局作用域跟局部作用域。。

所以闭包产生的环境应该就是在作用域当中了, 所以有闭包肯定有局部作用域, 有局部作用域并不一定有闭包~~

 

闭包的作用:

竟然有闭包的存在肯定有其存在的价值, 那么闭包的作用体现再哪里呢~~

var a = 1;
function fn1() {
    console.log(a); //1

    var b = 2;
}

console.log(b); //b is not defined
//当全局中有个变量a,我们在函数fn1中可以随时访问/修改这个变量,但是当我的函数fn1中有一个变量2的时候,我想在外面访问到这个b,很明显,失败了, 因为外部作用域是无法访问内部作用域的;

而闭包要做的事情就是让我们可以访问到变量b!

function fn1() {
    var b = 2;
    return function() {
        return b;
    }
}

var b = fn1()();
console.log(b);  //2

显然,我们拿到了b的值,这达到了我们想要的效果, 但是,为什么这样可以拿到b的值呢?

  • 在返回的函数中,我们是可以拿到b的值得,因为内部函数是可以访问外部作用域中的任何变量;
  • 在返回的函数中,我们把这个值也给返回了,所有fn1中就会一直存在一个b=2的值;
  • 一般fn1函数调用后里面的b会被销毁,但是因为我们再它的返回函数中调用了这个b,所以这个b会一直存在于内存中,不会被销毁(这也是闭包的弊端之一,后面详解);
  • 这样,我们利用闭包的原理,把b的值给悄悄偷出来了~

从上面这个小例子中我们就可以知道,闭包的作用就是可以让外面不能拿到变量值得地方可以顺利的拿到这些值,虽然外部函数不能拿到内部函数的值是JS对其作用域的一种保护,虽然闭包破坏了这种保护,但却实现了一些通常情况下不能实现的功能。

 

闭包的一些使用场景:

闭包的使用场景真的是无处不在,虽然有可能你本身并没有察觉到,但是并不能否定它的存在~

在说闭包的一些实际应用的地方之前,先看一段JS中的闭包经典小案例~~

for(var i=1; i<=5; i++) {
    setTimeout(function timer() {
        console.log(i);  //结果并不是我们想要的1-5, 而是连续出现5次6
    }, i*1000);
}

为什么会是这样呢? 因为作用域问题, 之前说过有闭包就有作用域~ 这里只有个全局作用域, 在全局中只有一个i,所以每次改变i都只是对其赋予一个新的值;

在setTImeout方法执行时, 循环已经结束了,所以每次循环都是i++直到循环完成,变成了6;

因为JS中没有块级变量, 为了让每次i的值我们都可以拿到,所以我们要创造一个作用域用来存储变量i的值, IIFE(函数自执行)可以创造一个块级变量:

for(var i=1; i<=5; i++) {
    (function(j) { //2,将i的值赋值给J
        setTimeout(function timer() {
            console.log(j);  //3,j输出的就是每次循环中i的值
        }, i*1000);
    })(i)  //1,每次循环拿到i的值
}
//执行结果    1,2,3,4,5

这样就实现的我们想要的结果,前面说,JS没有块级变量,如果有的话,是不是会更简单,ES5没有,但是ES6中有, 那么用块级变量怎么实现:

for(let i=1; i<=5; i++) {  //就是let
    setTimeout(function timer() {
        console.log(i); 
    }, i*1000);
}
//1,2,3,4,5

 

在实际的工作用,我们可能经常解除到的JS模块化,模块化的实现就是利用的JS中的闭包~~~

function moduleFn() {
    var name = 'just';

    var arr = [1,2,3];

    function nameFn() {
        console.log(name);
    }

    function arrFn() {
        console.log(arr.join("!"));
    }

    return {  //产生闭包! 在当前函数中返回了一个对象,这个对象中有nameFn,和arrFn, 这两个函数中又有了moduleFn中的变量。
        nameX: nameFn,
        arrX: arrFn
    }
}

var foo = moduleFn();  

foo.nameX(); //just
foo.arrX();    //1!2!3

//利用闭包,我们就可以获取作用域中的值了!~~

 

正是因为闭包的这种特性,在很多场景中可以利用闭包大展身手~  但是,高手使用大招是需要废除内力的~~而闭包的内力~~~

 

闭包的弊端:

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值

 

能实现功能固然重要,但是保证性能也同样重要,这其中的取与舍,只有在不断的项目实战中去自由拿捏~

如果能对闭包的使用驾轻就熟,那么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(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?

 

答案自宫之后便会知道!~


评论: