【JavaScript】闭包学习

前言

本文参考资料

《你不知道的JavaScript》上卷
《深入理解ES6》
《JavaScript高级程序设计》(第四版)
JavaScript深入之闭包


一、闭包是什么?

MDN对闭包的定义:

闭包是指那些能够访问自由变量的函数

《JavaScript高级程序设计》对闭包的定义:

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

在《JavaScript权威指南》中说:

所有的JavaScript函数都是闭包

这个听起来是不是有点奇怪?但是仔细想想的确如此,理论上,所有的函数,它们都在创建的时候将上层上下文的数据保存起来了,也就是说,它们会自动记住使用隐藏[[Environment]]属性创建的它们的位置,然后它们的代码可以访问外部变量
从实践的角度上来说,以下的函数才算是闭包:1. 即使创建它的上下文已经销毁,它仍然存在;2. 在代码中引用了自由变量(自由变量就是在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。)

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 ——《你不知道的JavaScript》上

二、理解闭包

function foo() {
	var a = 2;
	function bar() {
		console.log( a ); // 2
	}
	bar();
}
foo();

这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释bar()对a的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。

让我们看看这段代码,清晰地展示了闭包:

function foo() {
	var a = 2;
	function bar() {
		console.log( a );
	}
	return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。

在foo()执行后,其返回值(也就是内部的bar()函数赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。

bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用

bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用

bar()依然持有对该作用域的引用,而这个引用就叫作闭包

因此,在几微秒之后变量baz被实际调用(调用内部函数bar),不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问变量a。

这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

三、你可能没有意识到的闭包

其实闭包的身影处处可见,让我们来看看下面这段代码:

function wait(message) {

	setTimeout( function timer() {
		console.log( message );
	}, 1000 );
}
wait( "Hello, closure!" );

将一个内部函数(名为 timer)传递给setTimeout(…)。timer 具有涵盖 wait(…) 作用域的闭包,因此还保有对变量 message 的引用。

wait(…) 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait(…)作用域的闭包。

深入到引擎的内部原理中,内置的工具函数 setTimeout(…) 持有对一个参数的引用,这个
参数也许叫作 fn 或者 func,或者其他类似的名字。引擎会调用这个函数,在例子中就是
内部的 timer 函数,而词法作用域在这个过程中保持完整。

这就是闭包。

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包

IIFE

通常认为IIFE是典型的闭包例子,但根据先前对闭包的定义,严格来讲它并不是闭包

var a = 2;
(function IIFE() {
	console.log( a );
})();

这段代码中IIFE并不是在它本身的词法作用域外执行的。它在定义时所在的作用域中执行。而a是通过普通的词法作用域查找而非闭包发现的

循环与闭包

for (var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
		},i*1000 );
}

这段代码在运行时会以每秒一次的频率输出五次 6。为什么?

  1. 延迟函数的回调会在循环结束时才执行,即使每个迭代器执行的是setTimeout(…, 0),所有的回调函数依然是在循
    环结束后才会被执行,因此会每次输出一个 6 出来。
  2. 循环终止的条件是i不再<=5,条件首次成立时i的值是6。所以输出显示的是循环结束时i的最终值。

这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?

缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是
根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

要想达到目的,我们可以这样

for (var i=1; i<=5; i++) {
	(function(j) {
		setTimeout( function timer() {
			console.log( j );
		}, j*1000 );
	})(i);
}

利用IIFE来创造更多的词法作用域,这里不传i进去的话,IIFE的作用域是空的,所以它需要有自己的变量,用来在每个迭代中储存i的值。

我们还可以这样

for (let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

四、一些练习题

一道经典面试题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0](); //3
data[1](); //3
data[2](); //3

改成闭包版本:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();  //0
data[1]();  //1
data[2]();  //2

这道题很有意思!

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

举例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()返回结果。

总结与思考

其实闭包并没有想象中难,只要理解并记住闭包的定义以及作用,就非常好理解闭包函数的运转机制了,做题也so easy!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值