記一道經典的 JavaScript 面試題
最近,在溫習前端知識時,看到一道非常好的 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 的綜合能力,包含了變數定義提升
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()
進行建構函式的呼叫,原理同問題三。
如果你覺得我說的不清楚,還請說出來,一起交流。如果覺得有錯的地方,萬望指出。