JavaScript 闭包、函数作用域与运算符优先级经典面试题解析
本文深入解析 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;
}
利用上述兼容函数测试判断是否为匿名函数:

测试结果表明,变量 `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 构造函数传递一个表示函数体的字符串,返回包含该字符串命令的函数对象。通过此方法创建的函数均为匿名函数。

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报错

测试案例 2:非对象内部的函数表达式:
var fn=function (){
console.log(fn);
};
fn();//function (){console.log(fn);};正确

结论如下:在使用 `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 运算符优先级表:

通过查阅上表可知,点运算符(.)的优先级高于 new 操作符,因此相当于:
new (Foo.getName)();
所以实际上将 `getName` 函数作为构造函数来执行,因此弹出 2。
第六题解析
第六问 `new Foo().getName()`,首先看运算符优先级,括号高于 new,实际执行为:
(new Foo()).getName()
因此先执行 `Foo` 函数,而 `Foo` 此时作为构造函数却有返回值,这里需要说明 JavaScript 中构造函数的返回值问题。
构造函数的返回值规则
在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。
而在 JavaScript 中,构造函数可以有返回值也可以没有。
1、若无返回值,则按照其他语言一样返回实例化对象。

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

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

原题中,`Foo` 返回的是 `this`,而 `this` 在构造函数中本来就代表当前实例化对象,因此最终 `Foo` 函数返回实例化对象。
之后调用实例化对象的 `getName` 函数,因为在 `Foo` 构造函数中没有为实例化对象添加任何属性,因此到当前对象的原型对象(prototype)中寻找 `getName`,找到了。
因此最终输出 3。
第七题解析
第七问 `new new Foo().getName();`,同样是运算符优先级问题。
最终实际执行为:
new ((new Foo()).getName)();
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
相关标签:
