一道掉头发的前端 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();//
往期推荐