js闭包总结及常见闭包相关面试题

js闭包总结及常见闭包相关面试题

638发表于2019-06-20

一、什么是闭包

闭包是函数和声明该函数的词法环境的组合。闭包是很多语言都具备的特性,在js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等.
了解闭包首先了解js的‘链式作用域’结构,对象可以一级一级的向上查找父对象的变量,所以父对象的变量对子对象可见,反之不成立(向下不行);所以都可以访问全局变量(比如window,document,location等)

当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到上一级的链上找,一旦找到了变量,则不再继续.如果找到最后也没找到需要的变量,则解释器返回undefined.

了解了作用域链,我们再来看看js的内存回收机制,一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了.对应的内存空间也就被回收了.下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用.但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的.并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题.如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了.所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来.也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收.
也就是说,有了闭包,嵌套的函数结构才可以运作,这也是符合我们的预期的。

·为了解决函数外部无法访问函数内局部变量的问题,就在父函数中定义一个子函数并让它访问了父函数的局部变量,再通过父函数返回该子函数就实现了调用局部变量的效果;

·而上面中的子函数就是闭包,所以闭包的定义是“能够读取其他函数内部变量的函数”,其实简单理解就是“一个定义在函数中的函数”;
示例

function parent(){
 var a=1,b=2;
 function child(){
	console.log(a+b);
 }
 return child;
}
var func1=parent(); //这是将子函数返回给变量
func1(); 


二、闭包的常见写法

1、一个函数(即外部函数)里面包含另一个函数(即内部函数),并且return返回这个内部函数,然后内部函数在定义内部函数之外的作用域被调用,这时的内部函数就是一个闭包了。

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

2、内部函数引用了外部函数的变量,这个变量不会被销毁,因为闭包需要这个变量,所以通过闭包可以访问闭包保存的变量

function foo(){
   var n = 1;
   function innerFoo(){
		n +=  1;
		console.log(n);
	}
  return innerFoo;
}
var func = foo();
func(); // 2, 产生闭包innerFoo(),变量n保存在内存中
func(); // 3



三、闭包的优点及用途

1、通过闭包可以实现了js私有属性和私有方法的效果,js模块化封装一些高级的功能集,很多js源码你都能找到闭包的身影。
2、可以读取函数内部的变量。变量放在函数内部,不污染全局变量
3、可以把变量始终保存在内存中,可以用来作为数据缓存或计算功能。

四、闭包缺点

闭包的变量保存在内存中,内存泄漏,对内存的消耗很大,所以不要滥用闭包。否则会造成网页性能问题,IE可能会造成内存溢出。最好的方式是在退出闭包前将不必要的局部变量删除。减少闭包使用可以用立即执行函数传递变量。

五、常见闭包相关面试题

1、案例一

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=function(){
            alert(i)
        }
    }
};
foo();
result[0](); // 3
result[1](); // 3
result[2](); // 3

上面都是弹出3,循环退出是i是3。同理下面一也一样效果:

2、案例二

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。

原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。


要解决“案例一”中的闭包带来的负作用(案例二也可以通过这种方式),可以通过js立即执行函数。

var result=[];
function foo(){
    var i= 0;
    for (;i<3;i=i+1){
        result[i]=(function(j){
            return function(){
                alert(j);
            };
        })(i);
    }
};
foo();
result[0](); // 0
result[1](); // 1
result[2](); // 2
3、案例三

通过闭包使用参数。

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
4、案例四

闭包在js模块化中的应用:

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

5、案例五

function test(){
	var n=4399;
	function add(){
		n++;
		console.log(n);
	}
	return {n:n,add:add}
}
var result=test();
var result2=test();
result.add();//4400
result.add();//4401
console.log(result.n);//4399
result2.add();//4400

6、案例六

function f1(){
    //闭包f2被外引用始终存在内存中,而它所依赖的f1也会始终存在
    //所以n,m都是始终存在,删除闭包直接f1=null;
	n=1,m=100;
	madd=function(){
		m+=10;
		alert(m);
	}
	function f2(){
		n+=1;
		alert(n);
	}
	return f2;
}
var a=f1();
a();//2
madd();//110  //madd的值是一个匿名函数,同时是一个闭包,实现了外部对函数内部进行操作。
a();//3


参考:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

https://blog.csdn.net/rongmingye/article/details/82589223

https://www.cnblogs.com/mzwr1982/archive/2012/05/20/2509295.html
https://blog.csdn.net/zhouzuoluo/article/details/80723699

小编蓝狐