js 闭包详解

JavaScript 中的闭包是相当重要的概念,并且与作用域相关知识的指向密切相关,在大厂的前端面试过程中经常会被提及。

作用域基本介绍

JavaScript 的作用域通俗来讲,就是指变量能够被访问到的范围,在 JavaScript 中作用域也分为好几种,ES5 之前只有全局作用域和函数作用域两种。ES6 出现之后,又新增了块级作用域,下面我们就来看下这三种作用域的概念,为闭包的学习打好基础。

全局作用域

在编程语言中,不论 Java 也好,JavaScript 也罢,变量一般都会分为全局变量和局部变量两种。那么变量定义在函数外部,代码最前面的一般情况下都是全局变量。

在 JavaScript 中,全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量。下面通过看一段代码来说明一下什么是全局的作用域。

var globalName = 'global';

function getName() { 

  console.log(globalName) // global

  var name = 'inner'

  console.log(name) // inner

} 

getName();

console.log(name); // 

console.log(globalName); //global

function setName(){ 

  vName = 'setName';

}

setName();

console.log(vName); // setName

console.log(window.vName) // setName

从这段代码中我们可以看到,globalName 这个变量无论在什么地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的。

如果在 JavaScript 中所有没有经过定义,而直接被赋值的变量默认就是一个全局变量,比如上面代码中 setName 函数里面的 vName 变量一样。

我们可以发现全局变量也是拥有全局的作用域,无论你在何处都可以使用它,在浏览器控制台输入 window.vName 的时候,就可以访问到 window 上所有全局变量。

当然全局作用域有相应的缺点,我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题。

函数作用域

在 JavaScript 中,函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域,下面我们来看一段代码。

function getName () {

  var name = 'inner';

  console.log(name); //inner

}

getName();

console.log(name);

上面代码中,name 这个变量是在 getName 函数中进行定义的,所以 name 是一个局部的变量,它的作用域就是在 getName 这个函数里边,也称作函数作用域。

除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的。

下面再来看最后一个块级作用域。

块级作用域

ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。

听起来好像还不是很能理解块级作用域的意思,那么我们来举个更形象例子,看看到底哪些才是块级作用域呢?其实就是在 JS 编码过程中 if 语句及 for 语句后面 {…} 这里面所包括的,就是块级作用域。

下面结合一段代码来说明。

console.log(a) //a is not defined

if(true){

  let a = '123';

  console.log(a); // 123

}

console.log(a) //a is not defined

从这段代码可以看出,变量 a 是在 if 语句{…} 中由 let 关键词进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义。

那么有了上面这几种作用域的概念做铺垫之后,下面我们就可以来学习闭包的概念。

什么是闭包

红宝书闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数

闭包的基本概念

闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。

因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。下面我们通过代码先来看一个简单的例子。

function fun1() {

	var a = 1;

	return function(){

		console.log(a);

	};

}

fun1();

var result = fun1();

result();  // 1

结合闭包的概念,我们把这段代码放到控制台执行一下,就可以发现最后输出的结果是 1(即 a 变量的值)。那么可以很清楚地发现,a 变量作为一个 fun1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,我们最后还是可以拿到 a 变量的值。

现在结合着上面那段闭包的概念,你是否能很清晰地了解闭包的作用了呢?不清楚的话,可以再多体会一下这段代码。

闭包产生的原因

我们在前面介绍了作用域的概念,那么你还需要明白作用域链的基本概念。其实很简单,当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

需要注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。那么我们还是通过下面的代码来详细说明一下作用域链。

var a = 1;

function fun1() {

  var a = 2

  function fun2() {

    var a = 3;

    console.log(a);//3

  }

}

从中可以看出,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。

那么这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。

由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用。那么还是拿上的代码举例。

function fun1() {

  var a = 2

  function fun2() {

    console.log(a);  //2

  }

  return fun2;

}

var result = fun1();

result();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7nR9iJc-1617865990877)(https://s0.lgstatic.com/i/image2/M01/06/F3/Cip5yGAGp-qABNSNAAUwQ25mVao838.png)]

闭包三要素:- 外层函数包裹内层函数- 内层函数引用外层函数的变量- 外层函数返回内层函数

闭包的表现形式

那么明白了闭包的本质之后,我们来看看闭包的表现形式及应用场景到底有哪些呢?我总结了大概有四种场景。

  1. 返回一个函数,上面讲原因的时候已经说过,这里就不赘述了。

  2. 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。请看下面这段代码,这些都是平常开发中用到的形式。

// 定时器

setTimeout(function handler(){

  console.log('1');

},1000);

// 事件监听

$('#app').click(function(){

  console.log('Event Listener');

});
  1. 作为函数参数传递的形式,比如下面的例子。
var a = 1;

function foo(){

  var a = 2;

  function baz(){

    console.log(a);

  }

  bar(baz);

}

function bar(fn){

  // 这就是闭包

  fn();

}

foo();  // 输出2,而不是1

4. IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量,如下所示。

var a = 2;

(function IIFE(){

  console.log(a);  // 输出2

})();

IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,我们经常能在高级的 JavaScript 编程中看见此类函数。

以上关于闭包的基本概念、产生的原因及表现形式这三个方面,你已经有了一定的了解。那么最后一部分我们来看一个比较常见的开发应用场景。

如何解决循环输出问题?

在互联网大厂的面试中,解决循环输出问题是比较高频的面试题,一般都会给一段这样的代码让你来解释,那么结合本课时所讲的内容,我们在这里一起看看这个题目,代码如下。

for(var i = 1; i <= 5; i ++){

  setTimeout(function() {

    console.log(i)

  }, 0)

}

上面这段代码执行之后,从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么一般面试官都会先问为什么都是 6?我想让你实现输出 1、2、3、4、5 的话怎么办呢?

因此结合本讲所学的知识我们来思考一下,应该怎么给面试官一个满意的解释。你可以围绕这两点来回答。

setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。

因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

那么我们再来看看如何按顺序依次输出 1、2、3、4、5 呢?

利用 IIFE

可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。

for(var i = 1;i <= 5;i++){

  (function(j){

    setTimeout(function timer(){

      console.log(j)

    }, 0)

  })(i)

}

可以看到,通过这样改造使用 IIFE(立即执行函数),可以实现序号的依次输出。

使用 ES6 中的 let

ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过改造后的代码,可以实现上面想要的结果。

for(let i = 1; i <= 5; i++){

  setTimeout(function() {

    console.log(i);

  },0)

}

从上面的代码可以看出,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。

定时器传入第三个参数

setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下。

for(var i=1;i<=5;i++){

  setTimeout(function(j) {

    console.log(j)

  }, 0, i)

}

总结:

由于闭包会使一些变量一直保存在内存中不会自动释放,所以如果大量使用的话就会消耗大量内存,从而影响网页性能。因此,你更应该深入理解闭包的原理,从而保证交付的代码性能更好。

问:setTimeout为什么是闭包,这点没懂
讲师回复:

讲的广义的闭包,凡是在内存中执行完不会被马上清理的

问:关于闭包,还是觉得看作对象毕竟好理解;就好像在devtool中看到的closure也是健值对;老师说的可能是广义的吧?我理解的是嵌套函数中存在定义在外部的对象,所以需要开辟另外的内存空间存储这些属性,等到这些外部函数执行完毕销毁了,对应的属性还是保存在这个closure对象中。不知道老师怎么看的呢

问: 用let改造循环问题,感觉说的不够详细啊。let是改变了setTimeout的执行顺序了吗,为什么他能够按顺序输出呢

讲师回复: 上面有讲块级作用域,let就是ES6新增的,我个人是觉着太简单,大家应该都知道
讲师回复: 广义的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值