闭包和递归时函数中的两个比较特殊的情况,并且都被告诫人们要慎用。闭包相当于函数的返回值为另一个函数,而递归则相当于函数的参数为另一个函数。
一、闭包
- 概念:能够访问其他函数内部的变量的函数。也可以理解为在一个函数内部创建的函数。
- 优点:变量可以长期存在、可以避免全局变量的污染
- 缺点:消耗内存、会在父函数外部改变内部变量值
- 用途:可以读取函数内部的变量、让这些变量的值始终保持在内存中
function fn(){ var n = 9; addN = function (){ n += 1; }; return function (){ alert(n); } } var res = fn(); res();//9 addN(); res();//10
所谓读取函数内部的变量,其实就是,在函数内部创建的函数可以读取到父函数内部的变量,再把读取到的变量作为返回值,这样,函数外部也可以获取到函数内部的变量了。上面这段代码中,函数的返回值是一个闭包函数,被赋给了一个全局变量,这也就意味着函数fn的返回值将一直存在于内存中。第一次执行完res结果为9,第二次执行完res结果为10,表明了变量n是一直存在于函数内部的。
- 特点:只能取得包含函数中任何变量的最后一个值、函数嵌套、变量不被回收
- 销毁:手动设置变量为null,释放变量
- 应用
<button>click1</button> <button>click2</button> <button>click3</button> <button>click4</button> <button>click5</button> <script> var btn = document.getElementsByTagName('button'); for(var i = 0; i<btn.length; i++) { //原始写法 // btn[i].onclick = function() { // alert(i) // 不论点击第几个li,都弹出5 // } //闭包写法可以使结果为点击不同的li弹出对应的数字 // 闭包写法1 (function(i) { btn[i].onclick = function() { alert(i); } })(i); // 闭包写法2 btn[i].onclick = (function(i) { return function() { alert(i); } })(i) } </script>
对于原始写法执行后弹出的都是5的原因是:onclick是一个异步执行的事件,当事件被触发时,for循环以及结束,因此i的值也就已经变成了5,事件顺着作用域链由内向外查找到的i值为5。而使用闭包后,每次循环的i值都被保留下来,这样事件每次触发后寻找到的i值就是保留下来的i值。原始写法中的var如果用ES6中的let来代替也可以达到目的。
- 最后,献上阮一峰老师的两个栗子来进一步理解闭包的运行机制(又是令人困惑的this):
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()());
MDN是一个挺好用的网站,可以详细参考其中关于闭包的更多解释:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
二、递归
- 概念:在函数中调用该函数本身,形成递归。每次调用一个函数,为其分配新的空间,即被调函数返回时,调用函数中的变量仍然会保持原先的值,否则也不可能实现反向输出。函调时,每次调用要做地址保存、传参等,这是通过一个递归工作栈实现的。保存的内容有:局部变量、形参、调用函数地址、返回值等。
- 优点:简洁高效
- 缺点:效率低(内存消耗大、占用资源多、递归太深会导致栈溢出、存在重复计算)
- 特点:自我调用且有完成状态,必须有结束条件,否则会形成死循环。每一级函调时都有自己的变量,但函数代码并不复制,都有一次返回。
- 注意点:位于递归调用前的语句和各级被调函数,具有相同的执行顺序;位于递归调用后的语句的执行顺序与各各个被调函数的顺序相反。
- 实现方法:通过函数名称调用自身、通过arguments.callee调用自身、通过命名函数调用自身
- 与循环的区别及选择:循环方法比递归方法速度快,因为循环避免了一些重复的计算和额外开销,但循环不能解决所有问题。 一般情况下, 应尽量避免使用递归,如果很难建立一个循环,那就使用递归吧。
- 应用:
//斐波那契数列:1 1 2 3 5 8... function fib(n){ if(n == 0 || n == 1){ return 1; } else{ return fib(n-1)+fib(n-2); } }
//阶乘(红宝书中的经典栗子) //原始代码(利用函数名称调用自身) function fact(num){ if(num<=1){ return 1; }else{ return num*fact(num-1); } } var anotherFact = fact; fact = null; anotherFact(3);//出错 //改进代码1(利用arguments.callee调用自身),但是这种写法在严格模式下会出错 function fact(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); } } var anotherFact = fact; fact = null; anotherFact(3);//6 //改进代码2(利用命名函数调用自身) var fact=(function(){ 'use strict'; return function sum(num){ if(num<=1){ return 1; }else{ return num+sum(num-1); } } })(); fact(5);//15 var anotherFact = fact; anotherFact(5);//15 fact = null; anotherFact(5);//15
//求和1 function numAdd(n){ if(n = 0 || n = 1){ return n; }else{ return n + numAdd(n-1); } } //求和2 function celeNum(num){ if(num <= 10){ return num; }else{ return num%10 + celeNum(parseInt(num/10)); } }