`

大部分人都会做错的经典JS闭包面试题(转)

阅读更多
由工作中演变而来的面试题

这是一个我工作当中的遇到的一个问题,似乎很有趣,就当做了一道题去面试,发现几乎没人能全部答对并说出原因,遂拿出来聊一聊吧。

先看题目代码:


function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
1
2
3
4
5
6
7
8
9
10
11
function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?


//问:三行a,b,c的输出分别是什么?
1
//问:三行a,b,c的输出分别是什么?


这是一道非常典型的JS闭包问题。其中嵌套了三层fun函数,搞清楚每层fun的函数是那个fun函数尤为重要。

可以先在纸上或其他地方写下你认为的结果,然后展开看看正确答案是什么?


//答案:
//a: undefined,0,0,0
//b: undefined,0,1,2
//c: undefined,0,1,1
1
2
3
4
//答案:
//a: undefined,0,0,0
//b: undefined,0,1,2
//c: undefined,0,1,1


都答对了么?如果都答对了恭喜你在js闭包问题当中几乎没什么可以难住你了;如果没有答案,继续往下分析。

JS中有几种函数

首先,在此之前需要了解的是,在JS中函数可以分为两种,具名函数(命名函数)和匿名函数。

区分这两种函数的方法非常简单,可以通过输出 fn.name 来判断,有name的就是具名函数,没有name的就是匿名函数

注意:在低版本IE上无法获取具名函数的name,会返回undefined,建议在火狐或是谷歌浏览器上测试
或是采用兼容IE的获取函数name方法来获取函数名称:


/**
    * 获取指定函数的函数名称(用于兼容IE)
    * @param {Function} fun 任意函数
    */
function getFunctionName(fun) {
    if (fun.name !== undefined)
        return fun.name;
    var ret = fun.toString();
    ret = ret.substr('function '.length);
    ret = ret.substr(0, ret.indexOf('('));
    return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
    * 获取指定函数的函数名称(用于兼容IE)
    * @param {Function} fun 任意函数
    */
function getFunctionName(fun) {
    if (fun.name !== undefined)
        return fun.name;
    var ret = fun.toString();
    ret = ret.substr('function '.length);
    ret = ret.substr(0, ret.indexOf('('));
    return ret;
}
遂用上述函数测试是否为匿名函数:

image

可以得知变量fn1是具名函数,fn2是匿名函数

创建函数的几种方式

说完函数的类型,还需要了解JS中创建函数都有几种创建方法。

1、声明函数

最普通最标准的声明函数方法,包括函数名及函数体。


function fn1(){}
1
function fn1(){}


2、创建匿名函数表达式

创建一个变量,这个变量的内容为一个函数


var fn1=function (){}
1
var fn1=function (){}
注意采用这种方法创建的函数为匿名函数,即没有函数name


var fn1=function (){};
getFunctionName(fn1).length;//0
1
2
var fn1=function (){};
getFunctionName(fn1).length;//0


3、创建具名函数表达式

创建一个变量,内容为一个带有名称的函数


var fn1=function xxcanghai(){};
1
var fn1=function xxcanghai(){};
注意:具名函数表达式的函数名只能在创建函数内部使用
即采用此种方法创建的函数在函数外层只能使用fn1不能使用xxcanghai的函数名。xxcanghai的命名只能在创建的函数内部使用

测试:


var fn1=function xxcanghai(){
    console.log("in:fn1typeof fn1,">xxcanghai:typeof xxcanghai,">");
};
console.log("out:fn1typeof fn1,">xxcanghai:typeof xxcanghai,">");
fn1();
1
2
3
4
5
var fn1=function xxcanghai(){
    console.log("in:fn1typeof fn1,">xxcanghai:typeof xxcanghai,">");
};
console.log("out:fn1typeof fn1,">xxcanghai:typeof xxcanghai,">");
fn1();


//out:fn1xxcanghai:undefined >
//in:fn1xxcanghai:
1
2
//out:fn1xxcanghai:undefined >
//in:fn1xxcanghai:
可以看到在函数外部(out)无法使用xxcanghai的函数名,为undefined。

注意:在对象内定义函数如var o={ fn : function (){…} },也属于函数表达式


4、Function构造函数

可以给 Function 构造函数传一个函数字符串,返回包含这个字符串命令的函数,此种方法创建的是匿名函数。

image



5、自执行函数


(function(){alert(1);})();
(function fn1(){alert(1);})();
1
2
(function(){alert(1);})();
(function fn1(){alert(1);})();
自执行函数属于上述的“函数表达式”,规则相同



6、其他创建函数的方法

当然还有其他创建函数或执行函数的方法,这里不再多说,比如采用 eval , setTimeout , setInterval 等非常用方法,这里不做过多介绍,属于非标准方法,这里不做过多展开



三个fun函数的关系是什么?

说完函数类型与创建函数的方法后,就可以回归主题,看这道面试题。

这段代码中出现了三个fun函数,所以第一步先搞清楚,这三个fun函数的关系,哪个函数与哪个函数时相同的。


function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      //...
    }
  };
}
1
2
3
4
5
6
7
8
function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      //...
    }
  };
}


先看第一个fun函数,属于标准具名函数声明,是新创建的函数,他的返回值是一个对象字面量表达式,属于一个新的object。

这个新的对象内部包含一个也叫fun的属性,通过上述介绍可得知,属于匿名函数表达式,即fun这个属性中存放的是一个新创建匿名函数表达式。

注意:所有声明的匿名函数都是一个新函数。
所以第一个fun函数与第二个fun函数不相同,均为新创建的函数。



函数作用域链的问题

再说第三个fun函数之前需要先说下,在函数表达式内部能不能访问存放当前函数的变量。



测试1,对象内部的函数表达式:


var o={
  fn:function (){
    console.log(fn);
  }
};
o.fn();//ERROR报错
1
2
3
4
5
6
var o={
  fn:function (){
    console.log(fn);
  }
};
o.fn();//ERROR报错
image



测试2,非对象内部的函数表达式:


var fn=function (){
  console.log(fn);
};
fn();//function (){console.log(fn);};正确
1
2
3
4
var fn=function (){
  console.log(fn);
};
fn();//function (){console.log(fn);};正确
image

结论是:使用var或是非对象内部的函数表达式内,可以访问到存放当前函数的变量;在对象内部的不能访问到。

原因也非常简单,因为函数作用域链的问题,采用var的是在外部创建了一个fn变量,函数内部当然可以在内部寻找不到fn后向上册作用域查找fn,而在创建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。



所以综上所述,可以得知,最内层的return出去的fun函数不是第二层fun函数,是最外层的fun函数。

所以,三个fun函数的关系也理清楚了,第一个等于第三个,他们都不等于第二个。



到底在调用哪个函数?

再看下原题,现在知道了程序中有两个fun函数(第一个和第三个相同),遂接下来的问题是搞清楚,运行时他执行的是哪个fun函数?


function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
1
2
3
4
5
6
7
8
9
10
11
function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?


//问:三行a,b,c的输出分别是什么?
1
//问:三行a,b,c的输出分别是什么?


1、第一行a


var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
1
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
可以得知,第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以:

第后面几个fun(1),fun(2),fun(3),函数都是在调用第二层fun函数。

遂:

在第一次调用fun(0)时,o为undefined;

第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2,0);所以o为0

第四次同理;

即:最终答案为undefined,0,0,0



2、第二行b


var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
1
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
先从fun(0)开始看,肯定是调用的第一层fun函数;而他的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数。

遂:

在第一次调用第一层fun(0)时,o为undefined;

第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用 .fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时时(1,0)所以n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1;

第四次调用 .fun(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层fun函数为fun(3,2);所以o为2;

即最终答案:undefined,0,1,2



3、第三行c


var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
1
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
根据前面两个例子,可以得知:

fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,遂c存放的是fun(1)的返回值,而不是fun(0)的返回值,所以c中闭包的也是fun(1)第二次执行的n的值。c.fun(2)执行的是fun(1)返回的第二层fun函数,c.fun(3)执行的也是fun(1)返回的第二层fun函数。

遂:

在第一次调用第一层fun(0)时,o为undefined;

第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用 .fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;

第四次.fun(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数fun(3,1),所以o还为1

即最终答案:undefined,0,1,1



后话

这段代码原本是在做一个将异步回调改写为同步调用的组件时的代码,发现了这个坑,对JS的闭包有了更深入的了解。

关于什么是闭包,网上的文章数不胜数,但理解什么是闭包还是要在代码中自己去发现与领悟。

如果要我说什么是闭包,我认为,广义上的闭包就是指一个变量在他自身作用域的被使用了,就叫发生了闭包。



希望读者能通过本文对闭包现象有进一步的了解,如有其它见解或看法,欢迎指正或留言讨论。

原文链接:http://web.jobbole.com/84328/
分享到:
评论

相关推荐

    一道JS前端闭包面试题解析

    总结来说,这个面试题主要考察了JavaScript的闭包机制。闭包的关键在于,内部函数可以记住并访问创建它的外部函数的作用域,即使外部函数已经完成执行。这种特性在函数式编程和模块化设计中非常有用,可以用来封装...

    js常见面试题

    JavaScript是Web开发中不可或缺的一部分,尤其在前端领域,它的地位尤为重要。...在压缩包文件“面试题”中,可能会包含这些概念的实际题目,建议逐一解答并深入研究,以提高自己的JavaScript技能。

    深入理解javascript原型和闭包

    深入理解javascript原型和闭包(01)——一切都是对象 深入理解javascript原型和闭包(02)——函数和对象的关系

    python高阶闭包练习题

    通过以上的知识点,你可以开始尝试解决“python高阶闭包练习题”中的问题。在实践中应用这些概念,你的Python编程技能将会得到显著提升。同时,不断地练习和探索,你会发现闭包不仅是解决问题的工具,更是开启Python...

    前端面试题库,包含不限于Vue面试题,React面试题,JS面试题,HTTP面试题,工程化面试题,CSS面试题

    3. **JS面试题**:JavaScript是前端开发的基础,面试中会涵盖ES6+的新特性(如箭头函数、类、Promise、async/await等),原型链,闭包,作用域,异步编程,数据结构与算法等。 4. **HTTP面试题**:理解HTTP协议对于...

    js面试题下载

    面试题集合通常包含各种问题,旨在考察候选人在JS基础、jQuery库以及Ajax技术方面的理解和应用能力。现在,让我们深入探讨这些关键知识点。 1. **JavaScript基础**: - 变量与数据类型:了解`var`, `let`, `const`...

    Javascript 闭包完整解释

    ### JavaScript闭包完整解释 #### 一、闭包的基本概念 **闭包**是一个非常重要的JavaScript概念,它指的是一个函数能够记住并访问其外部作用域中的变量的能力,即使该函数在其外部作用域之外被调用也是如此。具体...

    js闭包个人理解

    ### JavaScript闭包的理解 在JavaScript中,闭包(Closure)是一种非常重要的概念,它涉及到函数作用域、变量生命周期以及函数内部对外部作用域的访问等多个方面。本文将基于提供的文件内容,深入探讨JavaScript...

    js面试技巧,面试题总结,MK

    这份"js面试技巧,面试题总结,MK"文档显然包含了作者在学习过程中整理的JS面试重点和常见问题,对于准备JS面试或者巩固基础知识的人来说是一份宝贵的资料。 在JS面试中,以下知识点经常会被考察: 1. **基础概念*...

    前端面试题库,包含Vue面试题React面试题JS面试题HTTP面试题

    3. **JS面试题**: JavaScript是前端开发的基础,面试中会涉及到语言特性、数据类型、作用域、闭包、原型链、异步编程(回调、Promise、async/await)、事件循环、错误处理等。 4. **HTTP面试题**: 理解HTTP协议...

    js闭包详细讲解

    ### JavaScript闭包详解 #### 一、闭包概念与特性 **闭包**是JavaScript语言的一个重要特性,它使得函数可以访问并操作其外部作用域内的变量,即使该函数在其外部作用域之外被调用。要理解闭包,首先需要了解...

    百度javascript前端面试题

    百度JavaScript前端面试题总结 本文总结了百度前端JavaScript面试题,涵盖了多个知识点,包括左定右自适应布局、GetPosition方法、模拟fadeIn和fadeOut、call和apply的区别和应用场景、创建a标签点击弹出对应序号、...

    JS 闭包的理解

    JavaScript中的闭包是一种重要的编程概念,它涉及到函数、作用域和变量持久化等多个核心知识点。在深入理解闭包之前,我们需要先了解JavaScript的作用域规则。 1. **作用域**:在JavaScript中,变量的作用域分为两...

    js常见经典面试题汇总

    ### JavaScript 常见经典面试题汇总解析 #### 1. JS的数据类型 JavaScript的数据类型主要分为两大类:**基本数据类型(primitive types)**和**引用数据类型(reference types)**。 - **基本数据类型**包括:`...

    JS原生面试题.zip

    这份“JS原生面试题.zip”资源显然是一份专门针对JavaScript的面试题集锦,旨在帮助求职者,尤其是初级开发者更好地准备企业面试。下面,我们将深入探讨其中可能涉及的一些关键知识点。 1. **基础语法**: - 变量...

    前端75道经典面试题.rar

    "前端75道经典面试题.rar"这个压缩包提供了互联网大厂常问的75道前端面试题,覆盖了基础到进阶的多个层面,是提升你面试能力的理想资料。以下是一些可能包含在这些面试题中的关键知识点: 1. **HTML与CSS**: - ...

    JS闭包经典

    闭包一点即通 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

Global site tag (gtag.js) - Google Analytics