【JavaScript】(八)闭包

一、理解闭包

        闭包是指有权访问另一个函数作用域中的变量的函数,闭包常见的创建方式就是在一个函数内部创建另一个函数。

看一个直接的例子:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

        在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。从本质上讲,makeAdder 是一个函数工厂 , 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

        add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

例子2:

function keith(){
    var a=1;
    function rascal(){
        return a;
    }
    return rascal;
}
var result=keith();
console.log(result()); //1

function keith(){
    var a=1;
    return function(){
        return a;
    };
}
var result=keith();
console.log(result()) //1
        上面代码中,两种写法相同,唯一的区别是内部函数是否是匿名函数。函数rascal就在函数keith内部,这时keith内部的所有局部变量,对rascal都是可见的。但是反过来就不行,rascal内部的局部变量,对keith就是不可见的。这就是JavaScript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。函数keith的返回值就是函数rascal,由于rascal可以读取keith的内部变量,所以就可以在外部获得keith的内部变量了。

        闭包就是函数rascal,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如rascal记住了它诞生的环境keith,所以从rascal可以得到keith的内部变量。

        闭包可以使得它诞生环境一直存在。看下面一个例子,闭包使得内部变量记住上一次调用时的运算结果。

function fn(num){
   return function(){
      return num++;
   };
}
var result = fn(2);
console.log(result());   //2
console.log(result());   //4
console.log(result());   //6
        上面代码中,参数num其实就相当于函数fn内部定义的局部变量。通过闭包,num的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包result使得函数fn的内部环境,一直存在。
通过以上的例子,总结一下闭包的特点:
  • 在一个函数内部定义另外一个函数,并且返回内部函数或者立即执行内部函数。
  • 内部函数可以读取外部函数定义的局部变量
  • 让局部变量始终保存在内存中。也就是说,闭包可以使得它诞生环境一直存在。

二、立即调用的函数表达式(IIFE)

        通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

循环中的闭包

        一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号:

for(var i = 0; i< 10; i++){
    setTimeout(function(){console.log(i);},1000);  //10
}
        上面代码中,不会符合我们的预期,输出数字0-9。而是会输出数字10十次。
        当匿名函数被调用的时候,匿名函数保持着对全局变量 i 的引用,也就是说会记住i循环时执行的结果。此时for循环结束,i 的值被修改成了10。

        为了得到想要的效果,避免引用错误,我们应该使用IIFE来在每次循环中创建全局变量 i 的拷贝。

for(var i = 0; i < 10; i++){
    (function(e){
        setTimeout(function(){console.log(e);1000}) 
    })(i);
}
        外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

猜你喜欢

转载自blog.csdn.net/qq_35768238/article/details/80591203