【js基础】怎么理解javascript中的闭包?

时间:2022-08-26 10:36:11

表现

感觉好像打破了javascript的作用域的约束,外层的作用域可以访问里面作用域的变量;

function foo(){
var a=5;
function baz(){
console.log(a);
}
return baz;
}
var f=foo();
f();//5

显然,变量f是在全局作用域中,a变量在foo函数的作用域中,但是在全局作用域中却访问并输出了a的值。
分析得,执行完foo()函数时,使f指向函数baz,接着执行f()语句时,即执行函数baz(),而baz()函数得作用域中包含函数foo()得作用域,还包含全局变量得作用域,所以可以访问到a的值。

所以我称为表现,其实本质上还是,里面的作用域访问外层的作用域中的变量。

本质

上述的例子是为了展示闭包,人为地修饰代码结构,现在,我们可以看一下二个经典的闭包例子。例一取自《javascript高级程序设计》第三版,例二取自《你不知道的javascrit》上卷。原理都一样。

例一:

    function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}

var r=createFunctions();
for(var j=0;j<r.length;j++){
console.log(r[j]());
}

如果我们不认真看一眼,十分容易得出 打印 0 1 2 3 4 5 6 7 9的结论。
其实是打印10个10,为什么会这样呢?下面我从内存的角度分析一下:
当执行var r=createFunctions();这条语句时,会调用createFunctions()函数,当执行for循环例里面的语句时,首先会使result[i]指向匿名函数,
当for结束后,i此时的值为10,而此时result是一个数组,保存着10个指向匿名函数的指针(本质上就是指针),虽然这十个指针都指向同一个匿名函数,但其实指向是不同的。可以讲10个result[i]分别指向10个大小相同的不同堆内存块中,而这十个对内存块中却引用同一个值i。
用图表示如下:
【js基础】怎么理解javascript中的闭包?

返回的result里面包含十个指针,分别指向不同内存块,返回时,在执行ri,时,即执行匿名函数,匿名函数会沿着作用域找i的值,在createFunctions(),函数中找到i的值,即for循环执行完后的值,所以i=10;所以最后输出10个10。

这个和c语言中让十个指针指向同一块内存,再让其中一个指针改变一下这块内存中的内容,然后在分别输出10个指针指向的内容差不多,肯定都输出改变后的内容。只不过这里内容还要沿着作用域链找一下而已。

那么问题来了,怎么可以让其输出 0-9呢?
最简单的方法,我们使用result=(function(){})() 称为立即执行函数(IIFE),直接返回值给每个result[i],而不让他为指针,直接为值(虽然指针和值本质上也是一样的)。如下:

    function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=(function(){
return i;
})();//注意这里
// result[i]=new Function("i","return i");
}
return result;
}

var r=createFunctions();
for(var j=0;j<r.length;j++){
console.log(r[j]);//注意这里
}

第二种方法:考虑到出现10个10 的本质,是因为在执行r [i] ()时,会调用匿名函数,然后沿着作用域链,在createFunctions()函数中找到i,由于for循环,i已经自增到10,所以打印10个10。通过上面分析,我们直到直到沿着作用域链找到createFunctions()的作用域里,就无法改变输出10个10 的结果。所以我们就想能不能在沿着作用域链找到createFunctions()函数的作用域之前就找到i的值呢?如下:

function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){

result[i]=function(num){
return function(){
return num; //注意这里
};
}(i);


}
return result;
}

var r=createFunctions();
for(var j=0;j<r.length;j++){
console.log(r[j]());
}

这里result返回数组同样包含十个指针,分别指向function(){return num;}在执行ri时,也会沿着作用域链向上找到第一个匿名函数,且时立即执行函数(IIFE),所以每次执行result[i]=function(num){}(i)时;会给num赋相应的值,所以在第一个匿名函数中就找到了num的值,即为每次传进来的i的值。

这里用可以表示为:

【js基础】怎么理解javascript中的闭包?

例二:

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

通过上面的分析,这次我们小心翼翼分析一下:当i=1时,执行setTimeout(function timer(){console.log(i);},i*1000);表示1s后才执行function匿名函数,由于javascript是单线程的语言,所以执行i++,会比匿名函数执行快很多,所以等到i变为6时,至此,已经调用5次setTimeout在javascript的任务队列中等待执行,分别需要等1s,2s,3s,4s,5s才依次执行,当第一次执行匿名函数时,i已经6,所以沿着原型链找到全局作用域中的i=6,所以输出6,易得,后面全输出6,所以结果为5个6;

那么怎么才可以输出我们期望的 1 2 3 4 5 呢?

类比上面的分析,如果不想输出6,则一定不能让i的值在全局环境中得到。那我们就要想办法再加局部环境。再延伸至全局环境中提前找到。

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

这样就行了,让i再第一个匿名函数的作用域中找到。注意写法。function外面的括号不能省略哦,否则就违反了js语法,若省略,在做语法分析时,会认为函数声明后面加了一个括号,这就会报错了。如果function前面再点东西,让编译器解释时,只要不认为是函数声明就行了,比如模仿jquery ,加一个+号。

最后,其实闭包本身不是很复杂,往往感到复杂,一是由于和this 匿名函数,立即执行函数(IIFE)等别的知识结合,搞得很复杂,二是:我们不敢用内存这样去理解,感觉这样很外行,其实很多时候我们的理解不一定要体现这门语言的特色,只要我们理解了这个知识点,并且熟练且不出错的运用到实际编程中,即使我们在理解这个知识点时是完全外行的,那也有关系。