最近,在温习前端知识时,看到一道非常好的 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 Foo
, new运算符 与 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.prototype
(newObj.__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面试题 )