JavaScript 前端面试高频陷阱:Foo.getName 七问七答全解析

作者:闭包旅人时间:2026-10-29 21:43:12浏览:116

本文深入解析一道极具代表性的 JavaScript 面试题,涵盖变量声明提升、this 指向、运算符优先级及原型链查找机制。通过 Foo.getName 的七个不同调用场景,详细剖析函数声明与表达式的执行顺序、构造函数返回值规则及全局变量污染问题,帮助开发者避开常见陷阱,掌握 JS 核心机制。

题目背景与考察目标

这道题目出自一套前端面试题的最后一题,旨在全面考核面试者的 JavaScript 综合能力。遗憾的是,在接近两年的时间里,几乎没有人能完全答对。这并非因为题目本身极其晦涩难懂,而是由于大多数面试者对其重视程度不足,容易掉以轻心。

具体题目如下所示:

完整代码与答案预览


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

这道题综合了笔者过往的开发经验以及在实际开发中遇到的各种 JavaScript 陷阱。题目涉及的知识点非常广泛,包括变量声明提升、this 指针指向、运算符优先级、原型链、继承机制、全局变量污染以及对象属性与原型属性之间的优先级判断等。

该题目共包含 7 个小问,下面将逐一进行详细解析。

第一问:静态属性访问

第一问解析

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

对于第一问 Foo.getName,其结果是访问 Foo 函数上存储的静态属性,因此输出结果为 2,这一点比较直观。

第二问:变量提升与函数表达式

第二问解析

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

1、变量声明提升

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

以下代码示例说明了这一机制:


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

当代码执行时,JavaScript 引擎会将声明语句提升至代码最上方,等价于执行以下代码:


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

2、函数表达式

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

第三问:作用域链与 this 指向

第三问解析

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

Foo 函数的第一句 getName = function () { alert (1); }; 是一条函数赋值语句,注意它没有使用 var 声明。因此,JavaScript 会先向当前 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 的运算符优先级问题。

题目背景与考察目标

题目背景与考察目标

通过查阅运算符优先级表可知,点(.)运算符的优先级高于 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 构造函数中没有为实例化对象添加任何属性,因此 JavaScript 会到当前对象的原型对象(prototype)中查找 getName,并成功找到。

因此最终输出结果为 3。

第七问:复杂调用链解析

第七问解析

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

最终实际执行顺序为:

new ((new Foo()).getName)(); 即先初始化 Foo 的实例化对象,然后将其原型上的 getName 函数作为构造函数再次执行 new 操作。

因此最终结果为 3。

答题情况总结与建议

总结

从答题情况来看,第一问的 100% 回答正确率表明基础概念较为简单;第二问的正确率约为 50%;第三问能回答正确的已不多见;而第四问能回答正确的则非常罕见。其实此题并没有太多刁钻匪夷所思的用法,都是一些开发者可能会遇到的实际场景。理论上,具备 1 年到 2 年工作经验的开发者应当能够完全正确作答。

只能说部分开发者过于急躁且轻视了基础细节,希望大家通过此文深入了解 JavaScript 的一些核心特性。

作者最新文章