前言

今天的这道题在之前的面试经历里面有提到过,但那篇阅读不是很多,但这道题是经典中的经典,为啥这道题很经典呢?

因为它结合了 函数作用域、原型、原型对象、运算符优先级、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 getNamefunction 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();//