1949啦网--小小 痛苦,是因为能力和欲望不匹配造成的

JavaScript中的匿名函数与闭包

Javascript中,闭包无疑是重要的一个环节,入门时多半会被弄晕。

// 匿名函数:没有名字的函数;

// 闭包:可访问一个函数作用域里的变量的函数;

匿名函数

// 普通函数      function box(){                       // 函数名是box;          return 'Lee';                           }      box();                                // =>Lee; 调用函数;  // 匿名函数      function(){                           // 匿名函数,会报错;          return 'Lee';      }  // 通过表达式自我执行      (function(name){          console.log(name);               // =>Lee;      })("Lee");                           // "()"表示执行函数,并且可以传参;  // 把匿名函数赋值给变量      var box = function(){                // 将匿名函数赋给变量;          return 'Lee';      };      console.log(box());                  // 调用方式和函数调用相似;  // 函数里的匿名函数      function box(){          return function(name){            // 函数里的匿名函数,产生闭包;              return name;          };      };      console.log(box()("Lee"));            // 函数box()调用匿名函数,并传参;

二 闭包

// 闭包:有权访问另一个函数作用域中的变量的函数;

// 创建闭包的常见方式:在一个函数内部创建另一个函数;通过另一个函数访问这个函数的局部变量;

// 通过闭包可以返回局部变量      function box(){          var user = 'Lee';          return function(){                // 通过匿名函数返回box()的局部变量user;              return user;          };      }      console.log(box()());                // =>Lee; 直接调用box()()来获得匿名函数的返回值;        var b = box();      console.log(b());                    // =>Lee; 另一种调用匿名函数方式;    // 优点:可以把局部变量驻留在内存中,可以避免使用全局变量;    // (全局变量污染导致应用程序不可预测性,每个模块都可调用必将引来灾难;所以推荐使用私有的,封装的局部变量);
// 缺点:  // 通过全局变量来累加      var age = 100;                        // 全局变量;      function box(){          age++;                            // 模块级可以调用全局变量,进行累加;      };      box();                                // 执行函数,累加一次;      console.log(age);                     // =>101; 输出全局变量;      box();                                // 执行函数,累加一次;      console.log(age);                     // =>102; 输出全局变量;  // 通过局部变量无法实现累加      function box(){          var age = 100;          age++;                            // 实现累加;          return age;      }      console.log(box());                   // =>101;      console.log(box());                   // =>101; 无法实现累加,因为第二次调用函数时,函数内部变量age又被初始化了;    // 通过闭包可以实现局部变量的累加      function box(){          var age = 100;          return function(){                // 匿名函数内实现累加;              age++;              return age;                   // 并返回累加后的变量;           };                                // 此时box()函数的局部变量age的值已经被修改为累加后的值;      }      var b = box();                        // 给box()函数赋值给变量;      console.log(b());                     // =>101; 调用匿名函数,累加一次;      console.log(b());                     // =>102; 第二次调用匿名函数,累加两次;    // PS:由于闭包里作用域返回的局部变量资源不会被立刻销毁回收,所以可能会占用更多的内存;所以过度使用闭包会导致性能下降;(将闭包引用在"私有作用域"中即可实现变量销毁)  // 作用域链的机制导致一个问题,在循环中里的匿名函数取得的任何变量都是最后一个值; ?
// 循环里包含匿名函数      function box(){          var arr = [];          for(var i=0; i<5; i++){         // 当声明变量i=5时,循环停止;而此时循环里的变量i==5;               arr[i] = function(){        // arr[i]得到的只是没有执行的匿名函数function(){};                  return i;                              };          };          return arr;                     // arr = [function,function,function,function,function];      }      var b = box();                      // =>[function,function,function,function,function]; 得到函数box()返回的数组arr;      console.log(b.length);              // =>5; 得到函数集合数组长度;      for(var i=0; i<b.length; i++){          console.log(box()[i]());        // =>5,5,5,5,5; 输出每个函数的值,都是最后一个值;      }      // 上面的例子输出的结果都是5,也就是循环后得到的最大i值;      // 因为b[i]调用的是匿名函数,匿名函数并没有自我执行,等到调用的时候,box()已执行完毕,i早已变成5;    // 循环里包含匿名函数-改1,自我执行匿名函数      function box(){          var arr = [];          for(var i=0; i<5; i++){              arr[i] = (function(num){    // arr[i]得到的是匿名函数执行后的结果数值0-4;                  return num;               })(i);                      // 自我执行并传参;          }          return arr;       }      var b = box();                      // =>[0,1,2,3,4];  此时b代表box()返回的数组;      for (var i = 0; i < b.length; i++) {          console.log(b[i]);              // 0 1 2 3 4; 这里返回的是数值;      };      // 例子中,我们让匿名函数进行自我执行,导致最终返回给a[i]的是数组而不是函数了;最终导致b[0]-b[4]中保留了0,1,2,3,4的值;    // 循环里包含匿名函数-改2,匿名函数里再做个匿名函数;      function box(){          var arr = [];           for(var i=0; i<5; i++){              arr[i] = (function(num){                  return function(){      // 返回函数;                      return num;                              }              })(i);          }          return arr;                     // arr = [function,function,function,function,function];      }      var b = box();      for (var i = 0; i < b.length; i++) {          console.log(b[i]());            // 0,1,2,3,4;       };    // 改1和改2中,我们通过匿名函数自我执行,立即把结果赋值给arr[i];  // 每一个i,是调用方通过按值传递的,所以最终返回的都是指定的递增的i;而不是box()函数中的变量i;

正式从下面开始详解:先从一个例子入手:

function lazy_sum(arr) {      var sum = function () {          return arr.reduce(function (x, y) {              return x + y;          });      }      return sum;  }     var f1 = lazy_sum([1, 2, 3, 4, 5]);  var f2 = lazy_sum([1, 2, 3, 4, 5]);  alert(f1 === f2); // false

本以为,f1和f2完全是两个完全一样的函数,但是实际上lazy_sum返回给f1和f2的两个匿名函数(参考本文第三部分)并不一样,即使看到“字面的表达式”是一样的,其实这涉及到更关键的一点是对于Javascript的“对象”的理解,这一点或许将来值得单独拿出来写篇文章。

一、闭包的定义

以上的lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种程序结构就被称为“闭包(Closure)”。(廖雪峰的定义)

在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。(阮一峰的的定义)

二、变量的作用域

要理解闭包,首先必须要明确的是Javascript特殊的变量作用域,也即全局变量和局部变量,这一点不难,可以简单地按照是否使用var声明变量来加以区分。

与变量的作用域相关的还有一个this方法,以及that指向。下面两段代码,在看完本文后不妨回来思考下如何解释两段代码的结果。

第一段代码:

var name = "The Window";  var object = {      name : "My Object",      getNameFunc : function(){          return function(){              return this.name;          };      }  };  alert(object.getNameFunc()());

第二代代码:

var name = "The Window";  var object = {      name : "My Object",      getNameFunc : function(){          var that = this;          return function(){              return that.name;          };      }  };  alert(object.getNameFunc()());

三、匿名函数

另一个需要明确的,就是匿名函数,举几个例子:

//普通函数   function box() {               //函数名是 box          return'Lee';   }        //匿名函数   function(){               //简单的匿名函数,单独使用会报错       return'Lee';   }        //通过表达式自我执行   (functionbox() {            //封装成表达式,单独使用会报错          alert('Lee');   })();                  //()表示执行函数,并且传参        //把匿名函数赋值给变量   var box=function(){            //将匿名函数赋给变量          return'Lee';   };   alert(box());            //调用方式和函数调用相似        //函数里的匿名函数   function box() {          return function(){                  //函数里的匿名函数,产生闭包               return'Lee';      }      }   alert(box()());               //调用匿名函数

四、闭包与内存

接下来就是闭包最让人疑惑的地方。

闭包最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

如何理解呢?从一个简单计数器入手:

function getCounter(){      var counter = 1;         var add = function() {          counter++;      }         var desc = function() {          counter--;      }         var get = function() {          return counter;      }         return {          add: add,          desc: desc,          get: get      }  };     var counter1 = getCounter();  var counter2 = getCounter();     console.log(counter1 === counter2)    //false     console.log(counter1.get())    //1  console.log(counter2.get())    //1     counter1.add()  counter1.add()  counter1.add()     counter2.desc()  counter2.desc()     console.log(counter1.get())    //4  console.log(counter2.get())    //-1     counter1 = null;  counter2 = null;

以上的例子可以看到,闭包内的函数add、desc可以反复调用,而父函数getCounter里的变量值则是在不断累加的。也就是说函数counter1及counter2中定义的父函数getCounter中的,由变量及子函数等一系要素构成该父函数的“运行环境”,一直停留在内存中,因而出现了累加的效果。

在JS的垃圾回收机制(garbage collection)中,如果一个变量发生过一次及以上的引用,那么这个变量就将停留在内存中,如以上代码,console.log(counter1.get())即是一次对counter1引用,所以这个getCounter()将会作为counter1的值一直停留在内存中,直到重新给counter1赋值,例如counter1 = null;就是一次清理内存的操作。在目前的小型脚本中,是不太会显示出内存占用问题的,而且现在新型的浏览器都有一定的内存控制机制,但是在古董级的IE浏览器中,可能就会面对严重的内存泄漏问题。所以及时清理无用内存是一个很好的习惯。

五、正确地使用闭包

function count() {      var arr = [];      for (var i=1; i<=3; i++) {          arr.push(function () {              return i * i;          });      }      return arr;  }     var results = count();  var f1 = results[0];  var f2 = results[1];  var f3 = results[2];     console.log(f1()); // 16  console.log(f2()); // 16  console.log(f3()); // 16

在这样一个程序中,我们期望得到的是[1,4,9],但是最终得到的却是[16,16,16]。为什么呢?

因为内存停留的原因啊,f1、f2、f3每一次赋值,返回的函数都引用了i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。

所以返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

那么如何解决呢?

再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

function count() {      var arr = [];      for (var i=1; i<=3; i++) {          arr.push((function (n) {              return function () {                  return n * n;              }          })(i));    //所谓“绑定”,即是通过`i`传参,每次返回函数时初始化      }      return arr;  }     var results = count();  var f1 = results[0];  var f2 = results[1];  var f3 = results[2];     console.log(f1()); // 1  console.log(f2()); // 4  console.log(f3()); // 9

六、大佬是这样玩的

几十年前,一位美国数学家阿隆佐·邱奇(Alonzo Church,1903年6月14日-1995年8月11日),提出了一套名为λ演算(Lambda calculus)的计算机运算逻辑,最简单的部分,不使用加减乘除以及数字,来进行四则运算。对的,如果让你算1+1,你可以伸出两个指头,让你算9x9,你要说你背得乘法口诀表,但是这些方法计算机并不理解啊,所以需要有一个方法可以让计算机实现以上得运算,于是λ演算就解决了这个问题,也因此今天你得以在计算器上执行各种复杂得运算,你以为显而易见的1+1等于2,其实背后有一套严密的运算逻辑(不要跟我讲哥德巴赫猜想,拒绝)。

那么我们用JavaScript来演示一下λ演算的逻辑吧:

'use strict';     // 定义数字0:  var zero = function (f) {      return function (x) {          return x;      }  };     // 定义数字1:  var one = function (f) {      return function (x) {          return f(x);      }  };     // 定义加法:  function add(n, m) {      return function (f) {          return function (x) {              return m(f)(n(f)(x));          }      }  }     // 计算数字2 = 1 + 1:  var two = add(one, one);     // 计算数字3 = 1 + 2:  var three = add(one, two);     // 计算数字5 = 2 + 3:  var five = add(two, three);     // 你说它是3就是3,你说它是5就是5,你怎么证明?     // 看这里:     // 给3传一个函数,会打印3次:  (three(function () {      console.log('print 3 times');  }))();     // 给5传一个函数,会打印5次:  (five(function () {      console.log('print 5 times');  }))();

其实这并不是真正意义上的加减乘除运算,而是通过循环引用制造的“计算假象”罢了 :P

原文链接:https://www.qiquanji.com/post/7292.html

本站声明:网站内容来源于网络,如有侵权,请联系我们,我们将及时处理。

微信扫码关注

更新实时通知

作者:xialibing 分类:网页教程 浏览: