一道掉头发的前端 JS 输出题
前言
今天的这道题在之前的面试经历里面有提到过,但那篇阅读不是很多,但这道题是经典中的经典,为啥这道题很经典呢?
因为它结合了 函数作用域、原型、原型对象、运算符优先级、this 指向、变量提升、函数声明提升 等一系列问题。同样也是字节跳动的真题,可能输出结果不一样,但万变不离其宗。
那么你完全弄明白了这一道题,基本上面试问到与之相关的输出题应该不成大问题了。
然而,这道题是最容易让人忽视,当你觉得自己 js 学的很好的时候,不妨来试一试吧,能做全对,面试遇到的话,可以说是一马当先,直通卡到手。
先不要着急看后面解析嗷,这里我放了图片,可以放大自己试着做一做,之后再看解析会有很大帮助。

询问输出结果

本文参考:一道常被人轻视的前端JS面试题 https://www.cnblogs.com/xxcanghai/p/5189353.html
第一问
从第 1 行往下看,第 1 到 4 行定义了一个叫「Foo的函数」。
第 5 行为 Foo 创建了一个叫 getName 的「静态属性」存储了一个匿名函数。
第 6 行为 Foo 的「原型对象」新创建了一个叫 getName 的匿名函数。
第 7 行通过「函数变量表达式」创建了一个 getName 的函数。
第 8 行声明一个叫 getName 函数。
回到第一个询问输出结果,Foo.getName,这个访问的是 Foo 函数上存储的「静态属性」,输出结果为 2 ,这个比较简单,没什么好说的,进入下一问。
第二问
第二问,直接调用 getName 函数。既然是直接调用那么就是访问「当前上下文作用域内」的叫 getName 的函数,所以跟 1 2 3 都没什么关系。
但是看到后面第 8 行代码,你想回答输出结果为 5 ,那么你就错了,就踩了这个问题的坑了。
“此处有两个坑,「一是变量声明提升,二是函数表达式。」
”
下面来分别解释一下,方便大家巩固知识:
「变量声明提升」
即所有声明变量或声明函数都会被提升到当前函数的顶部。例如下代码:
console.log(x); //undefined
var x;
x = 0;
代码执行时 js 引擎会将声明语句提升至代码最上方,变为:
var x;
console.log(x); //undefined
x = 0;
「函数表达式」
var getName与 function getName 都是声明语句,区别在于var getName 是「函数表达式」,而function getName是「函数声明」。
函数表达式最大的问题,在于 js 会将此代码拆分为两行代码分别执行。例如下代码:
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 函数。
同理,原题中代码最终执行时的是:

第三问
第三问的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 变量。
”
简单的讲,「this 的指向是由所在函数的调用方式决定的」。而此处的直接调用方式,this 指向 window对象。
如果这里不太明白的小伙伴,可以看一下之前推荐的李兵老师的课程,讲的很详细!
于是 Foo 函数返回的是 window 对象,相当于执行 window.getName() ,而 window 中的 getName 已经被修改为 alert(1),所以最终会输出 1。
此处考察了两个知识点,一个是「变量作用域问题」,一个是「this指向问题」。
不知道这里你的头发掉了多少了呢,稍作休息,我们继续下一问!

第四问
直接调用 getName 函数,相当于 window.getName() ,因为这个变量已经被 Foo 函数执行时修改了,那么结果与第三问相同,为1。
这里承接上一问,如果上一问没做出来,那么这一问估计也就跟着凉凉,可见这一道题是有多奇妙!
第五问
new Foo.getName(); ,此处考察的是 js 的运算符优先级问题。
js 运算符优先级:
(下图来自于 MDN-运算符优先级)

通过查上表可以得知点(.)的优先级高于 new 操作,相当于是:
new (Foo.getName)();
所以实际上将getName函数作为了构造函数来执行,显然输出结果为 2。
这一问应该比较简单吧,主要是考察优先级,没有上文那样还需要上一问的结果才能解出当前这问。
第六问
第六问 new Foo().getName(),首先看运算符优先级「括号高于 new」,并且带参数的 new 操作符是优先级最高的,实际执行为
(new Foo()).getName()
先执行 Foo 函数,而 Foo 此时作为构造函数却有返回值,所以这里需要说明下 js 中的「构造函数返回值问题」。
原题中,返回的是this,而 this 在构造函数中本来就代表当前实例化对象,最终 Foo 函数「返回实例化对象」。
之后调用实例化对象的 getName 函数,因为在「Foo构造函数中没有为实例化对象添加任何属性」,于是到当前对象的原型对象(prototype)中寻找 getName,找到了。
最终输出结果为 3。
第七问
有了上文基础之后,这一问就比较简单了,最终实际执行为:
new ((new Foo()).getName)();
先初始化 Foo 的实例化对象,然后将其原型上的 getName 函数作为构造函数再次 new 。
最终输出结果为 3。
答案

最后,考虑到可能大家想验证一下结果的正确性,为了方便大家复制,在这里贴下源代码,为你考虑这么周到了,就不要白嫖本文了嗷,点赞支持一下小狮子~
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();//
往期推荐
