1. 程式人生 > >記一道經典的 JavaScript 面試題

記一道經典的 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 的綜合能力,包含了變數定義提升

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() 進行建構函式的呼叫,原理同問題三。



如果你覺得我說的不清楚,還請說出來,一起交流。如果覺得有錯的地方,萬望指出。