什么是闭包?闭包的作用及应用场景

一、什么是闭包

假设,把下面三行代码放在一个立即执行函数中。
在这里插入图片描述
三行代码中,有一个局部变量local,有一个函数foo,foo里面可以访问到local变量。

好了这就是一个闭包:

「函数」和「函数内部能访问到的变量」的总和,就是一个闭包。

有的同学就疑惑了,闭包这么简单么?

「我听说闭包是需要函数套函数,然后 return 一个函数的呀!」

比如这样:

function foo(){
  var local = 1;
  function bar(){
    local++;
    return local;
  }
  return bar;
}
var func = foo()
func()

这里面确实有闭包,local变量和bar函数组成了一个闭包(Closure)。

为什么要函数套函数呢?

是因为需要局部变量,所以才把 local 放在一个函数里,如果不把 local 放在一个函数里,local 就是一个全局变量了,达不到使用闭包的目的——隐藏变量(等会会讲)。

有些人看到「闭包」这个名字,就一定觉得要用什么包起来才行。其实这是翻译问题,闭包的原文是 Closure,跟「包」没有任何关系。

所以函数套函数只是为了造出一个局部变量,跟闭包无关。

为什么要 return bar 呢?

因为如果不 return,你就无法使用这个闭包。把 return bar 改成 window.bar = bar 也是一样的,只要让外面可以访问到这个 bar 函数就行了。

所以 return bar 只是为了 bar 能被使用,也跟闭包无关。

二、闭包的作用

闭包常常用来「间接访问一个变量」。换句话说,「隐藏一个变量」。

假设我们在做一个游戏,在写其中关于「还剩几条命」的代码。

如果不用闭包,你可以直接用一个全局变量:

window.lives = 30 // 还有三十条命

这样看起来很不妥。万一不小心把这个值改成 -1 了怎么办。所以我们不能让别人「直接访问」这个变量。怎么办呢?

用局部变量。

但是用局部变量别人又访问不到,怎么办呢?

暴露一个访问器(函数),让别人可以「间接访问」。

代码如下:

!function(){

  var lives = 50

  window.奖励一条命 = function(){  // 简明起见,用了中文
    lives += 1
  }

  window.死一条命 = function(){
    lives -= 1
  }

}()

那么在其他的 JS 文件,就可以使用 window.奖励一条命() 来涨命,使用 window.死一条命() 来让角色掉一条命。

看到闭包在哪了吗?

在这里插入图片描述
闭包是 JS 函数作用域的副产品。换句话说,正是由于 JS 的函数内部可以使用函数外部的变量,所以这段代码正好符合了闭包的定义。而不是 JS 故意要使用闭包。

JS变量作用域存在于函数体中即函数体,并且变量的作用域是在函数定义声明的时候就是确定的,而非在函数运行时。只要你懂了 JS 的作用域,你自然而然就懂了闭包,即使你不知道那就是闭包。

三、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

四、思考题

如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。

代码片段一

var name = "The Window";

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

alert(object.getNameFunc()()); // The Window

代码片段二

var name = "The Window";

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

alert(object.getNameFunc()()); // My Object

五、应用场景

例子1

function fn(){
	var a = 1;
	return function(){
		return  ++a;                                     
	}
}
alert(fn()());
alert(fn()());  //每次执行都会初始化 a = 1
//2 2

例子2

function outerFn(){
  var i = 0; 
  function innerFn(){
      i++;
      console.log(i);
  }
  return innerFn;
}
var inner = outerFn();  //每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2();   
//1 2 3 1 2 3

例子3

(function() { 
  var m = 0; 
  function getM() { 
    return m; 
  } 
  function seta(val) { 
    m = val; 
  } 
  window.g = getM; 
  window.f = seta; 
})(); 
f(100);
console.log(g());   //100  闭包找到的是同一地址中父级函数中对应变量最终的值

例子4

var add = function(x) { 
  var sum = x; 
  var tmp = function(x) { 
      sum = sum + x; 
      return tmp;    
  } 
  tmp.toString = function() { 
      return sum; 
  }
  return tmp; 
} 
alert(add(1)(2)(3));     //6
alert(add(1)(2)(3)(4));  //10

上面代码中,例add(1)(2)(3),代码执行add(1)的时候,声明了add函数的局部变量sum并赋值为1,同时返回子函数tmp,这样add(1)(2)(3)就相当于tmp(2)(3),因为tmp函数需要用的sum这个变量,使得add执行完毕之后并没有清除sum这个局部变量的数据。这样执行tmp(2)的时候将2与sum相加保存在sum上,同时返回自身tmp。这时sum为3,tmp(2)(3)就相当于tmp(3),然后运行tmp(3),把3与sum相加保存在变量sum上,同时返回tmp。这时add(1)(2)(3)运行之后结果是tmp函数(sum=6),而用console.log()函数显示结果的时候会将里面的内容自动转换为字符串,所以console.log(tmp)相当于console.log(tmp.toString()),而这个toString()函数被重定义为return sum,所以结果就是console.log(sum)//6,以此类推如果后面还有括号那么sum将继续加下去到最后剩下tmp然后运行toString()返回结果。

点这里了解柯里化函数

例子5

function fun(n,o) {
    console.log(o);
    return {
         fun:function(m) {
               return fun(m,n);
         }
    };
}
var a = fun(0);  //undefined
a.fun(1);  //0  
a.fun(2);  //0  
a.fun(3);  //0  
var b = fun(0).fun(1).fun(2).fun(3);   //undefined  0  1  2
var c = fun(0).fun(1);  
c.fun(2);  
c.fun(3);  //undefined  0  1  1

例子6

//事件处理函数中闭包的写法
var lis = document.getElementsByTagName("li");
for(var i = 0; i < lis.length; i++){
    (function(i){
        lis[i].onclick = function(){
            console.log(i);
        };
    })(i);
}  

上面代码中,事件函数中用到了循环值i,利用闭包其实就是为了把当前的循环值传递进去。

例子7

function fn(){
   var arr = [];
   for(var i = 0;i < 5;i ++){
	 arr[i] = function(){
		 return i;
	 }
   }
   return arr;
}
var list = fn();
for(var i = 0,len = list.length; i < len; i ++){
   console.log(list[i]());
}  
//5 5 5 5 5

例子8

function fn(){
  var arr = [];
  for(var i = 0;i < 5;i ++){
	arr[i] = (function(i){
		return function (){
			return i;
		};
	})(i);
  }
  return arr;
}
var list = fn();
for(var i = 0,len = list.length; i < len; i ++){
  console.log(list[i]());
}  
//0 1 2 3 4

上面例子7和例子8代码对比,例子8在for循环里arr[i] = 这一句主要是赋予它一个闭包,赋予的是立即执行函数“(function(i{…}))(i)”,执行之后返回的函数保存了i的值。相比较例子7只是赋予了普通函数,返回的i在最后调用的时候才获取,这时候i已经变成5了, 如果把例子7的for循环中var i = 0改为let i = 0,结果就和例子8一样,因为let会保证i每次都是不同的。

参考文章

https://zhuanlan.zhihu.com/p/22486908
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
https://blog.csdn.net/weixin_43586120/article/details/89456183

  • 9
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值