本文深入剖析一道经典的JavaScript面试题Foo.getName()。文章详细拆解了静态属性、实例属性、变量提升、this指向以及new运算符的执行顺序。通过七个具体的调用场景,解释了从静态方法访问到全局变量覆盖,再到原型链查找的完整逻辑,帮助开发者彻底理解JavaScript中的作用域与对象创建机制。
接下来我们将探讨一道在JavaScript面试中极具代表性的题目。
该题目综合考察了静态属性与实例属性的区别、变量提升机制、this关键字的指向规则,以及使用new关键字调用函数的具体过程。
function Foo() {
getName = function () {
console.log(1);
};
return this;
};
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
};
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName(); 请尝试预测以下代码片段的最终输出结果。
Foo.getName(); //2
getName(); //4
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
new new Foo().getName(); //3一、题目解析
1. Foo.getName()
首先分析代码的上半部分初始化逻辑。代码定义了一个名为Foo的构造函数,随后为Foo函数对象本身添加了一个名为getName的静态属性,该属性指向一个匿名函数。接着,在Foo的原型对象(prototype)上定义了另一个名为getName的匿名函数。随后,通过函数表达式声明了一个全局变量getName,最后又通过函数声明语句定义了同名函数getName。
对于第一个调用Foo.getName(),直接访问的是Foo函数对象上的静态属性,因此输出结果为2。
二、解析
2. getName()
此处输出结果为4,主要考察变量提升与函数声明提升的优先级问题。在使用var声明变量时,会发生变量提升现象。例如,在变量声明之前访问该变量不会报错,而是返回undefined。
console.log(a)// undefined
var a = 1;
console.log(a)// 1由于声明提前的特性,上述代码在逻辑上等价于将var声明移至作用域顶部,而赋值操作保留在原位。
var a
console.log(a)// undefined
a = 1;
console.log(a)// 1同样地,函数声明(注意区分函数声明与函数表达式或构造函数)也存在声明提前的机制,允许在函数定义之前调用该函数。
fn() // 1
function fn() {
console.log(1);
};
fn() // 1
//因为函数声明提前,导致函数声明也会被提到代码顶端,所以等同于
function fn() {
console.log(1);
};
fn() // 1
fn() // 1当变量声明与函数声明同时存在时,谁的提升优先级更高?根据《你不知道的JavaScript》中的论述,函数声明的优先级高于变量声明。尽管两者都会提升,但函数声明会覆盖同名的变量声明。因此,题目中的代码执行顺序可理解为函数声明优先于变量声明被提升。
function getName() {
console.log(5);
};
var getName;
getName = function () {
console.log(4);
};基于上述提升规则,即可解释为何此处输出结果为4。
三、解析
3. Foo().getName()
执行Foo()函数时,函数内部将全局作用域中的getName变量重新赋值。在JavaScript中,若变量未使用var、let或const显式声明,则默认成为全局对象(在浏览器环境中为window)的属性。因此,全局的getName被修改。
当执行Foo()时,函数返回this。在非严格模式下,全局环境中的this指向window对象。由于Foo内部修改了全局的getName,此时调用this.getName()实际上等同于调用window.getName(),故返回结果为1。
四、解析
4. getName()
此时输出结果为1已无悬念。在前一步分析中,Foo函数的执行已经修改了全局变量getName的值。因此,再次直接调用getName()等同于调用window.getName(),输出结果自然为1。
五、解析
5. new Foo.getName()
首先需明确运算符的优先级。根据我的分析,优先级顺序为【new Foo() > Foo() > new Foo】(注:此处原文逻辑有误,实际new Foo.getName()中.优先级高于new,故先执行Foo.getName()再new)。先计算Foo.getName(),其结果为“2”(即执行静态方法),随后对该结果使用new操作符。由于new作用于数字2,这相当于对原始值进行包装,因此该过程执行了一遍静态方法并输出2。
六、解析
6. new Foo().getName()
此考点涉及new操作符的基本概念。该调用分为两步:第一步执行new Foo()生成一个实例对象,第二步在该实例上调用getName方法。
new一个构造函数的过程大致如下:首先以构造函数的原型(prototype)创建一个新的对象,使其继承原型链;接着调用构造函数,并将this指向这个新创建的对象,以便对象能继承构造函数内部定义的属性;如果构造函数没有显式返回一个对象,则默认返回这个新创建的对象。
因此,在执行new Foo()时,系统首先基于Foo.prototype创建了一个实例对象。由于Foo.prototype上预先定义了一个getName方法(即输出3的那个函数),该实例可通过原型链访问到此方法。此外,由于Foo构造函数内部未定义其他属性,最终返回的this指向该实例。此时,调用该实例的getName()方法,将沿原型链查找并执行原型上的getName,因此输出结果为3。
七、解析
7. new new Foo().getName()
该表达式等价于new (new Foo().getName())。
首先执行内部表达式new Foo().getName(),根据前文分析(第6点),其结果为3。随后,对该结果使用new操作符创建一个新的实例对象。因此,最终输出结果为3。
总结
至此,关于JavaScript面试题Foo.getName()的详细解析已介绍完毕。如需了解更多相关JavaScript面试题内容,欢迎查阅本站往期文章或继续浏览后续相关内容,感谢大家的支持!