对于前端人员面试,出现频率最多也是让人最头疼的就是面试官说:“请简单谈一谈你对闭包的理解”。对于这一个几乎快被人问烂的问题,屡屡出现在我们面试或被面试的过程中的原因很简单--我们一直都在接触闭包,却很少去正确地对待它。
因为闭包是因为JS的一些语言特性而形成的,所以在谈它之前我们首先要了解一下的知识点
1.执行上下文
2.作用域
3.垃圾回收机制
4.函数嵌套
本文只会简单的谈涉及到的内容,如果知识点有遗漏的同学可以自行google,接下来让我们进入正题!
1. 什么是闭包?
关于什么是闭包让我们先看看《高级程序设计》和《JavaScript权威指南》中的说法:
《高程》: 闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数。
《权威指南》:和其他大多数现代编程语言一样,JavaScript也采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链互相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”。
比较两种说法《高程》中的说法太过抽象,我比较倾向于《权威指南》中的说法,现在让我们剥茧抽丝,一步一步的解释这种说法。
2. 解释闭包,扫荡闭包理解过程中产生的知识点
我们知道“JavaScript中没有块级作用域”,所谓“块”,也就是大括号“{}”中间的语句(但是在ES6中已经引入块级作用域,这里不做讨论).我们还知道在JavaScript中,在函数里面定义的变量,可以在函数里面被访问,但是在函数外无法访问,这也就形成了函数作用域,即如下代码所示
var i = 1;
if(true){
var j = 2;
}
console.log(i,j) // 1 2
function test(){
var z = 3;
}
test();
console.log(z); //Uncaught ReferenceError: z is not defined(…)
又因为函数是可以嵌套的,所以函数A里面定义的函数B也能函数A内部声明的变量,即如下代码所示
function func() {
var num = 10;
function sub() {
console.log(num)
}
sub();
}
func(); //10
因为一层层的函数嵌套就形成了作用域链的概念,,但是如果我们仅仅只是这样去理解或者解释作用域或者作用域链就有些太过肤浅了,接下来,我们再在此基础上添加执行上下文以及垃圾回收机制再去深入理解他们,先让我们来看下面的代码
1 var a = 1, b= 1;
2 function func() {
3
4 var c = 10,
5 a = 10;
6
7 console.log(a);
8
9 function sub() {
10 var a = 100,
11 d = 100
12 console.log(a)
13 }
14 sub();
15 }
16 console.log(a); //1
17 func(); // 10
100
可以从开始来了解一下这一段代码的执行过程:
-
在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量就行赋值。
-
程序执行到第16行,调用console.log(a); 控制台输出 1
-
程序执行到第17行,调用func(),生成此次调用func函数时的上下文环境,压栈,并将此上下文环境设置为活动状态
-
程序执行到第7行,调用console.log(a); 因为在此上下文环境中 a = 10, 控制台输出 10
-
程序执行到第14行,调用sub(),生成此次调用sub函数的上下文环境,压栈,并设置为活动状态
-
程序执行到第12行,调用console.log(a); 因为在此上下文环境中 a = 100, 控制台输出 100
-
程序执行到第14行,调用sub()结束,sub函数上下文环境被销毁,回到func函数上下文环境,变为活动状态。
-
程序执行到第17行,调用func()结束,func函数上下文环境被销毁,全局上下文环境又回到活动状态
-
over
写到这里先说一下,以上所说的和我们经常所说的闭包都没有关系。。。>_< 别打脸!
但是!!不可避免的可以说,以上所说的都是闭包。让我们反过来,看看闭包的定义:
《高程》:闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数,这不就是我们刚刚的函数嵌套吗?
《权威指南》:和其他大多数现代编程语言一样,JavaScript也采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链互相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”。 这不就是我们刚刚研究的作用域链吗?
所有说,闭包的概念早就在我们的心中了,佛曰:不可说,不可说。。。
认真总结上面的知识点,并且在权威指南的定义中还给我们巩固了另一个很重要的知识点:函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的 , 所以当我们每次看到函数的调用环境和定义的环境不相同的时候是一定要谨慎谨慎再谨慎啊!
3. 闭包应用的场景
都说两天腿的人好找,三条腿的蛤蟆难寻。既然闭包这么好理解,那为啥老是出现在各种面试宝典中呢?其实这个很简单,总会有基因突变的蛤蟆不是么?>_<
因为JavaScript的另一大特性-函数是复杂数据类型的一种,所以就可以被返回,被传递,这就让闭包进行了基因突变。
1. 函数作为返回值
function func(){
var max = 10;
return function(){
consloe.log(max++);
}
}
var f = func();
var f1 = func();
f(); // 10
f(); // 11
f1();//10
我们很多时候都会想当然的根据一句话去定义一个问题如:“函数调用完成之后,其执行上下文环境就会被销毁”,但是在上面的代码中,函数调用完成之后,其执行上下文环境不会接着被销毁,并且连续执行两次func会产生两个func执行上下文环境
2.函数作为参数被传递
var max = 10;
function func(){
console.log(max);
}
function f1(fn){
var max =1000;
fn();
}
f1(func); //10
这就是我们上文所说的函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的,虽然被作为参数传递到函数f1内部被调用,但是他的上一级作用域依然是全局作用域。
好吧,先写到这吧,水水更健康。。。。