记一道经典的 JavaScript 面试题

时间:2021-01-04 14:15:18

最近,在温习前端知识时,看到一道非常好的 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();//第七题 ?

  先自己思考下,看自己能分析到第几步,最好能够拿出纸推算一下。

  这道题的经典之处在于它综合考察了面试者的 JavaScript 的综合能力,包含了变量定义提升this 指针指向运算符优先级原型继承全局变量污染对象属性原型属性优先级等知识。

  ps:大家也不要轻视这类题目,它虽然不太可能会出现在项目中,业务上很少有人这么写程序,但是可以通过这样的练习夯实自己的基础。

  此外如果在面试的时候碰到此类繁琐的题目,请记住面试官并不要求你能把所有题目都答对,主要是看看你是否了解其中的原理与思想,答对与答错对面试官来说,这不是主要的



这里附上腾讯面试官给现在找实习的同学的几条建议(在我看来也可以当做箴言来记住了):
1. “我们可以允许你不会,但是一旦会,就得深入理解。”
  面试官能够从面试中知道你的热爱,但是你的热爱要有所体现。这也就是为什么师兄会提醒我们,对于那些不太精通的技能尽量不要写在简历上面。比如说项目经历的时候,说到用到某门技术,但是一旦问到该技术的细微地方,就回答不清楚,这样面试下来的结局或许很悲剧。


2. “不要太在意网上或者同学提供的面试建议,要有意识培养个人的真正实力
  他提出这样一个例子:应该是网上和同学说了,腾讯的网页重构必问“优化”,于是某同学就背出了“减少http数”和“CDN”等,但是一旦问道为什么减少http数能够加快加载速度。要深入,要理解原理。他还说道,流传出来关于面试的礼仪,比如见到面试官要起立等等,他们都是不看重的。
  他说:有那么两种人,一种是知道自己不足在哪里,另外一种是知道自己不足在哪里然后尝试去改变。


3. “遇到不懂的,可以说一下思路,说一下自己的想法。说不定能够让面试官刮目相看

此段转自 一次实习生面试经历

解析


前面的闲扯有点长,也是为了留些空间让大家好好思考一下上面的题目。(哈哈,其实也算是有感而发~)

注:为了简单解释,后面我会用函数最后alert的数字,指代这个函数。

程序在编译阶段做了什么?

  为了更好理清复杂的程序,我们最好根据把程序在编译阶段的行为,将程序进行一次修改(此处省略问题程序的执行,仅针对相关函数声明的问题):

function Foo() {
    getName = function () { alert (1); };
    return this;
}
function getName() { alert (5);}

Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);};

  此处最大的变化是 函数5 位置的改变,这是函数提升,还有函数优先。

问题一
Foo.getName();  //2

  这是一个简单的对象属性访问的问题,可能有小伙伴会误认为3,如果在 getName 不是直接存在于 Foo 中,才会遍历[[Prototype]] 链,这时候才会取 Foo.prototype.getName。(即函数3)


问题二
getName();      //4

  在对程序在编译阶段的行为的分析中,经过函数提升,我们可以知道最后 getName() 被赋值为函数4


问题三
Foo().getName();    //1

  首先 Foo() 调用函数 Foo,使 getName() 被赋值为函数1,并且返回 this,此 this 为 window(在非严格模式下,且在浏览器环境下),这里涉及到 this 4条绑定原理 中的默认绑定
所以,上面的问题三可以进一步转换为:

window.getName();

  接着原理同问题二,调用函数1。


问题四
getName();      //1

  答案同问题三,原理同问题二


问题五
new Foo.getName();      //2

  这里需要知道各运算符的优先级(这里请照着 MDN 的表格一起来分析这一题:运算符优先级

  首先 new Foonew运算符 与 Foo 相连,new运算符 被当做无参数列表(优先级 18),不及 . 属性访问运算符(优先级19),先进行属性访问,便于理解,这里加个括号。

new (Foo.getName)();    //2

  将 Foo.getName 作为构造函数调用,普通函数作为构造函数调用首先会调用一次原函数,故 Foo.getName() 被调用。(即函数2,原理同问题一)


问题六
new Foo().getName();    //3

  首先 new Foo()new运算符 与 Foo() 相连,new运算符 被当做有参数列表(优先级19),与 . 属性访问运算符(优先级19)一致,级别一样高,从左到右,先遇到谁先计算谁。

Foo 作为构造函数调用时,会发生这些事情:
1. 创建一个新对象,该对象继承自 Foo.prototype
2. 将 this 绑定到这个新对象上;
3. 构造函数返回的对象就是 new表达式 的结果。此程序中 Foo() 返回 this(即这个新对象)

为了看得清楚,对问题进行一次分解:

var newObj = new Foo();     //构造函数创建的新对象
newObj.getName();

  newObj 即是构造函数返回的新对象,这个对象不会得到 Foo 上的属性,即没有 Foo.getName 属性(函数2),不过继承了Foo.prototypenewObj.__proto__ === Foo.prototype),在 newObj 上查找 getName,自身没有就到 newObj的原型链上找,最后在 newObj.__proto__ 上找到,即函数3。


问题七
new new Foo().getName();    //3

这个问题看着很吓人,其实如果你已经知道问题五和问题六的原理,这就不成问题。

这里首先调用的 第二个new运算符,然后才是 第一个new运算符,上面的程序可以进行一次分解:

var newObj = new Foo();
new (newObj.getName)();

实质是对 Foo.prototype.getName() 进行构造函数的调用,原理同问题三。



如果你觉得我说的不清楚,还请说出来,一起交流。如果觉得有错的地方,万望指出。

(原问题来源:前端程序员经常忽视的一个JavaScript面试题