JavaScript 闭包、函数作用域与运算符优先级经典面试题解析

作者:半夏时间:2026-10-22 10:05:22浏览:185

本文深入解析 JavaScript 面试中高频出现的闭包、函数类型与创建方式、变量声明提升以及运算符优先级等核心考点。通过具体代码案例,详细剖析闭包中变量引用的机制、函数表达式与声明的区别,以及 new 运算符与点运算符的优先级对执行结果的影响,帮助开发者掌握底层原理,应对复杂面试场景。

当前 JavaScript 程序员的面试流程中,上机编程或纸笔作答已成为主流形式,旨在直观考察候选人的逻辑思维与编码能力。为此,整理并解析了最新的 JavaScript 面试真题及其解题思路,供读者参考。

JavaScript 闭包与函数作用域深度解析

闭包相关题目解析


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的输出分别是什么?

//参考答案:


//a: undefined,0,0,0
//b: undefined,0,1,2
//c: undefined,0,1,1

若上述答案全部正确,说明你对 JavaScript 闭包的理解已相当扎实;若未能完全答对,请继续阅读后续的深入分析。

JavaScript 中函数的分类

在深入之前,需明确 JavaScript 中的函数主要分为两类:具名函数(命名函数)与匿名函数。

区分这两类函数的方法十分直接:通过输出 `fn.name` 属性进行判断,若存在 name 属性则为具名函数,否则为匿名函数。

需注意,在低版本 Internet Explorer 浏览器中,无法获取具名函数的 name 属性,其返回值将为 undefined。建议在 Firefox 或 Chrome 等现代浏览器中进行测试以获取准确结果。

为实现兼容 IE 浏览器的函数名称获取功能,可采用以下兼容方案:


/**
 * 获取指定函数的函数名称(用于兼容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;
}

利用上述兼容函数测试判断是否为匿名函数:

JavaScript 闭包与函数作用域深度解析

测试结果表明,变量 `fn1` 对应的是具名函数,而 `fn2` 对应的是匿名函数。

JavaScript 中创建函数的多种方法

明确了函数类型后,接下来探讨 JavaScript 中创建函数的各种方式。

1、函数声明语句

这是最标准且常见的函数定义方式,包含明确的函数名及函数体。


function fn1(){}

2、匿名函数表达式

创建一个变量,并将其赋值为一个函数。


var fn1=function (){}

需注意,采用此方式创建的函数属于匿名函数,即不具备 name 属性。


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

3、具名函数表达式

创建一个变量,并将其赋值为一个带有名称的函数。


var fn1=function xxcanghai(){};

关键点在于:具名函数表达式中的函数名仅在该函数内部可见。

这意味着,使用此方法创建的函数在外部只能通过变量名(如 `fn1`)调用,而无法直接通过内部指定的名称(如 `xxcanghai`)访问。该内部名称仅在函数作用域内有效。

测试代码:


var fn1=function xxcanghai(){
 console.log("in:fn1<",typeof fn1,">xxcanghai:<",typeof xxcanghai,">");
};
console.log("out:fn1<",typeof fn1,">xxcanghai:<",typeof xxcanghai,">");
fn1();
//out:fn1< function >xxcanghai:< undefined >
//in:fn1< function >xxcanghai:< function >

运行结果显示,在函数外部尝试访问 `xxcanghai` 会返回 undefined,证实了其不可见性。

补充说明:在对象字面量中定义的函数,例如 `var o={ fn : function (){…} }`,同样属于函数表达式范畴。

4、Function 构造函数

可以向 Function 构造函数传递一个表示函数体的字符串,返回包含该字符串命令的函数对象。通过此方法创建的函数均为匿名函数。

JavaScript 闭包与函数作用域深度解析

5、自执行函数


(function(){alert(1);})();
(function fn1(){alert(1);})();

自执行函数本质上属于上述的“函数表达式”,遵循相同的规则。

6、其他非常规创建或执行方式

此外还存在其他创建或执行函数的方法,例如使用 `eval`、`setTimeout`、`setInterval` 等。这些属于非标准或非常规用法,此处不再展开讨论。

三个 fun 函数的关系辨析

在明确了函数类型与创建方式后,回到原题。

题目代码中出现了三个 `fun` 函数,首要任务是厘清它们之间的关系,确定哪些函数是相同的。


function fun(n,o) {
 console.log(o)
 return {
 fun:function(m){
  //...
 }
 };
}

首先分析第一个 `fun` 函数,它属于标准的具名函数声明,是一个新创建的函数实例。其返回值是一个对象字面量表达式,代表一个新的 object 实例。

该新对象内部包含一个名为 `fun` 的属性,根据前述知识可知,该属性存放的是一个匿名函数表达式,即一个新的匿名函数实例。

需注意:所有声明的匿名函数都是独立的新函数实例。

因此,第一个 `fun` 函数与第二个 `fun` 函数并不相同,它们各自是新创建的独立函数。

函数作用域链的影响

在分析第三个 `fun` 函数之前,需先探讨函数表达式内部能否访问存放当前函数的变量。

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


var o={
 fn:function (){
 console.log(fn);
 }
};
o.fn();//ERROR报错

JavaScript 闭包与函数作用域深度解析

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


var fn=function (){
 console.log(fn);
};
fn();//function (){console.log(fn);};正确

JavaScript 闭包与函数作用域深度解析

结论如下:在使用 `var` 声明或定义在非对象内部的函数表达式中,可以访问到存放当前函数的变量;而在对象内部定义的函数表达式中,则无法访问到该变量。

原因很简单,这与函数作用域链有关。使用 `var` 声明时,`fn` 变量在外部作用域创建,函数内部查找不到时会向上层作用域查找。而在对象内部定义时,由于未在函数作用域内创建该变量,导致无法访问。

综上所述,最内层返回的 `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,?,?,?

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

1、第一行代码 a 的执行分析


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`(即 `n=0`)。内部调用第一层 `fun(2,0)`,故 `o` 为 0;

第四次调用同理;

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

2、第二行代码 b 的执行分析


var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?

从 `fun(0)` 开始,显然调用的是第一层 `fun` 函数;其返回值是一个对象,因此后续的 `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` 函数不是第一次执行的返回对象,而是第二次执行的返回对象。在第二次执行第一层 `fun` 函数时传入参数为 `(1,0)`,因此 `n=1`,`o=0`。返回时闭包了第二次的 `n`(即 `n=1`)。遂在第三次调用第三层 `fun` 函数时,`m=2`,`n=1`,即调用第一层 `fun` 函数 `fun(2,1)`,故 `o` 为 1;

第四次调用 `.fun(3)` 时,`m` 为 3。闭包了第三次调用时的 `n`(即 `n=2`)。同理,最终调用第一层 `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,?,?,?

根据前两个例子的分析逻辑,可以得出:

`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

题目:


function Foo() {
 getName = function () { alert (1); };
 return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

function Foo() {
 getName = function () { alert (1); };
 return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

答案:


Foo.getName();//2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3

经典面试题:函数声明、变量提升与运算符优先级

第一题解析

首先分析题目上半部分的代码逻辑:定义了一个名为 `Foo` 的函数,随后为 `Foo` 创建了一个名为 `getName` 的静态属性,存储了一个匿名函数;接着为 `Foo` 的原型对象(prototype)创建了一个名为 `getName` 的匿名函数;之后通过函数变量表达式创建了一个 `getName` 函数;最后再声明一个名为 `getName` 的函数。

第一问 `Foo.getName` 自然访问的是 `Foo` 函数上存储的静态属性,结果为 2,无需多言。

第二题解析

第二问直接调用 `getName` 函数。由于是直接调用,访问的是当前上下文作用域内的 `getName` 函数,因此与 1、2、3 均无直接关系。此题有无数面试者误答为 5。这里存在两个陷阱:一是变量声明提升,二是函数表达式。

变量声明提升机制

即所有声明的变量或函数都会被提升到当前函数的顶部执行。

示例代码如下:


console.log('x' in window);//true
var x;

x = 0;

代码执行时,JavaScript 引擎会将声明语句提升至代码最上方,变换为:


var x;
console.log('x' in window);//true
x = 0;

函数表达式的特殊性

`var getName` 与 `function getName` 均为声明语句,区别在于 `var getName` 属于函数表达式,而 `function getName` 属于函数声明。关于 JavaScript 中各种函数创建方式的详细区别,可参考文章《大部分人都会做错的经典JS闭包面试题》。

函数表达式最大的问题在于,JavaScript 会将此代码拆分为两行分别执行。

示例代码如下:


console.log(x);//输出:function x(){}
var x=1;
function x(){}

实际执行的代码为,先将 `var x=1` 拆分为 `var x;` 和 `x = 1;` 两行,再将 `var x;` 和 `function x(){}` 两行提升至最上方,变为:


var x;
function x(){}
console.log(x);
x=1;

因此最终函数声明的 `x` 覆盖了变量声明的 `x`,log 输出为 `x` 函数。

同理,原题中代码最终执行时的状态为:


function Foo() {
 getName = function () { alert (1); };
 return this;
}
var getName;//只提升变量声明
function getName() { alert (5);}//提升函数声明,覆盖var的声明

Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);};//最终的赋值再次覆盖function getName声明

getName();//最终输出4

第三题解析

第三问 `Foo().getName()` 先执行了 `Foo` 函数,然后调用 `Foo` 函数返回值对象的 `getName` 属性函数。

`Foo` 函数的第一句 `getName = function () { alert (1); };` 是一条函数赋值语句,注意它没有 `var` 声明。因此先向当前 `Foo` 函数作用域内寻找 `getName` 变量,未找到。再向当前函数作用域上层,即外层作用域内寻找是否含有 `getName` 变量,找到了,也就是第二问中的 `alert(4)` 函数,将此变量的值赋值为 `function(){alert(1)}`。

此处实际上是将外层作用域内的 `getName` 函数修改了。

需注意:若此处依然没有找到,会一直向上查找到 window 对象;若 window 对象中也没有 `getName` 属性,就在 window 对象中创建一个 `getName` 变量。

之后 `Foo` 函数的返回值是 `this`,而 JavaScript 的 `this` 指向问题在博客园中已有大量文章介绍,此处不再赘述。

简而言之,`this` 的指向由所在函数的调用方式决定。此处的直接调用方式,`this` 指向 window 对象。

因此 `Foo` 函数返回的是 window 对象,相当于执行 `window.getName()`,而 window 中的 `getName` 已被修改为 `alert(1)`,所以最终会输出 1

此处考察了两个核心知识点:变量作用域问题与 this 指向问题。

第四题解析

直接调用 `getName` 函数,相当于 `window.getName()`,因为这个变量已被 `Foo` 函数执行时修改,因此结果与第三问相同,为 1

第五题解析

第五问 `new Foo.getName();`,此处考察的是 JavaScript 的运算符优先级问题。

JavaScript 运算符优先级表:

JavaScript 闭包与函数作用域深度解析

通过查阅上表可知,点运算符(.)的优先级高于 new 操作符,因此相当于:


new (Foo.getName)();

所以实际上将 `getName` 函数作为构造函数来执行,因此弹出 2。

第六题解析

第六问 `new Foo().getName()`,首先看运算符优先级,括号高于 new,实际执行为:


(new Foo()).getName()

因此先执行 `Foo` 函数,而 `Foo` 此时作为构造函数却有返回值,这里需要说明 JavaScript 中构造函数的返回值问题。

构造函数的返回值规则

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。

而在 JavaScript 中,构造函数可以有返回值也可以没有。

1、若无返回值,则按照其他语言一样返回实例化对象。

JavaScript 闭包与函数作用域深度解析

2、若有返回值,则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(string, number, boolean, null, undefined),则与无返回值相同,实际返回其实例化对象。

JavaScript 闭包与函数作用域深度解析

3、若返回值是引用类型,则实际返回值为这个引用类型。

JavaScript 闭包与函数作用域深度解析

原题中,`Foo` 返回的是 `this`,而 `this` 在构造函数中本来就代表当前实例化对象,因此最终 `Foo` 函数返回实例化对象。

之后调用实例化对象的 `getName` 函数,因为在 `Foo` 构造函数中没有为实例化对象添加任何属性,因此到当前对象的原型对象(prototype)中寻找 `getName`,找到了。

因此最终输出 3。

第七题解析

第七问 `new new Foo().getName();`,同样是运算符优先级问题。

最终实际执行为:


new ((new Foo()).getName)();
作者最新文章