来探讨一下js中作用域与闭包的问题,一道经典的面试题

时间:2022-01-10 06:07:35

公司前端招人的时候,负责招聘伙伴想了这样一道题,但是自己解释的不是很清楚,也是难为了来参加面试的人.题目如下:

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

大概在js方面有过了解的人,大都知道这样一道充满迷惑性的题的答案是间歇的输出了5个5.以此为突破口来窥探一下js的冰山一角.



首先,这道题有两个可能只是偶尔接触js(比如我)不会注意到的知识点:

1.for循环中,循环语句是循环体内部的父作用域.也就是说,循环语句以及循环体内部的作用域是不一样的.经常见到在循环体内部使用循环语句的变量,其实相当于当函数内部找不到变量时,会向上查找.

2.js单线程的任务执行顺序.setTimeout属于注册事件,绑定的事件是当前事件队列(同步事件)执行完毕后再执行.

根据第2个知识点来说.这道题的结果就很好理解了,源代码相当于:

var i = 0;
setTimeout(
function() {
console.log(i);
}, 100
0);
i
++;
setTimeout(
function() {
console.log(i);
}, 2
000);
i
++;
setTimeout(
function() {
console.log(i);
}, 300
0);
i
++;
setTimeout(function() {
console.log(i);
}, 4000);
i++;
setTimeout(function() {
console.log(i);
}, 5000);
i++;

当同步任务完成后,也就是i自加后,setTimeout事件开始被触发,1000ms后打印出了i.这里要注意一下setTimeout这个函数的执行顺序,比如当100个延时1000ms的函数同时执行,那么这100个异步函数将同时执行.

 

看下一道题,不从异步函数来思考.

var test=function(){
var arr={};
for(var i =1;i<3;i++){
arr[i]
=function(){
return i*i;
}
}
return arr;
}

var a=test();

a[
1]();//?
a[2]();//?

请思考2分钟,再点开答案(当然如果你很确定答案了就直接点开吧)

来探讨一下js中作用域与闭包的问题,一道经典的面试题来探讨一下js中作用域与闭包的问题,一道经典的面试题
var test=function(){
var arr={};
for(var i =1;i<3;i++){
arr[i]
=function(){
return i*i;
}
}
return arr;
}

var a=test();

a[
1]();//9
a[2]();//9
View Code

如果你是一名像我一样很少把函数体直接赋值给变量的程序员,又或者你是一位c系列的程序员,你可能会相当困惑这个答案.产生这个困惑的原因是

  1.我以为函数中变量在内存中的存储方式是以常量的形式存储的(这句话不严谨,会在未来我醒悟的某一天来修改).

  2.产生1这种想法的原因是因为我的常识中是存在块级作用域的,而事实是这里并不存在块级作用域.

  3.暂时不表

 

简单且不负责任的描述一下作用域:当你声明一个变量后,你能在花括号"{}"中找到这个变量的所有区域,则是这个变量的作用域.

最常见的作用域就是函数体,特别是像我这种php程序员可能接触到的唯一的作用域就是函数体而产生的作用域.

 

接下来以我的愚见来解释一下这道题

当程序执行到

var a=test();时

 

a被声明成一个数组函数对象(姑且这么叫吧,是由2个函数组成的数组对象)

并且,函数中的i并不是常量,而是变量i.(这里可以抛出一个问题,当函数中的变量被赋值后,内存中存储的函数中的变量是以变量还是常量的形式存储)

 

那么,这两个函数体相同且均为

function(){
return i*i;
}

到这里,你可能会顿悟为什么a[1],a[2]输出的是同一个值了.如果还是对输出同一个值有疑问,请回到上一个并且再多探索探索.

接下来,就是这个i是多少的问题了.再看一下这道对于初学者来说邪恶的函数体:

var test=function(){
var arr={};
for(var i =1;i<3;i++){
arr[i]
=function(){
return i*i;
}
}
return arr;
}

i是在for语句的循环条件中声明的,当我们执行a[1]或者a[2]时,js将从声明js的作用域中去寻找i.

而i所在的作用域,则是整个test函数.

而此时i的值,是多少呢?不清楚的可以自己做个试验

for(var i=1;i<3;i++){
}
console.log(i); //3

如果对此感到疑惑的,说明你可能是一位勤恳扎实的c系列程序员,但是在js中花括号并不会单独生成一个作用域(块级作用域).

此事放眼函数体,唯一的作用域唯一的i,此时的值是3.

所以最后数组函数对象a的每一个函数体都相当于

function(){
return 3*3;
}

这也就是a[1]()==a[2]()==9的原因.

 

如果看到这里,你还存在很多疑惑,那就很对了.

因为写到这里的我依旧有很多不敢触及的分支,只敢畏畏缩缩的在我所熟悉的路上探索.这大概就是程序语言的魅力,一个细微的变量将影响到全局的走势.我会不定时更新这篇文章.