此部分以一道題目來作解釋javascript
題目以下:html
function Foo() { getName = function () { console.log(1); }; return this; } Foo.getName = function () { console.log(2);}; Foo.prototype.getName = function () { console.log(3);}; var getName = function () { console.log(4);}; function getName() { console.log(5);} //請寫出如下輸出結果: Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName();
答案是:java
function Foo() { getName = function () { console.log(1); }; return this; } Foo.getName = function () { console.log(2);}; Foo.prototype.getName = function () { console.log(3);}; var getName = function () { console.log(4);}; function getName() { console.log(5);} //答案: Foo.getName();//2 getName();//4 Foo().getName();//1 getName();//1 new Foo.getName();//2 new Foo().getName();//3 new new Foo().getName();//3
此題涉及的知識點衆多,包括變量聲明提高、this指針指向、運算符優先級、原型、繼承、全局變量污染、對象屬性及原型屬性優先級等等。面試
此題包含7小問,分別說下。閉包
先看此題的上半部分作了什麼,首先定義了一個叫Foo的函數,以後爲Foo建立了一個叫getName的 靜態屬性 存儲了一個匿名函數,以後爲Foo的 原型對象 新建立了一個叫getName
的匿名函數。以後又經過 函數變量表達式 建立了一個getName
的函數,最後再 聲明 一個叫getName
函數。函數
第一問的 Foo.getName
天然是訪問Foo函數上存儲的靜態屬性,天然是2,沒什麼可說的。this
第二問,直接調用 getName 函數。既然是直接調用那麼就是訪問當前上文做用域內的叫getName的函數,因此跟1 2 3都沒什麼關係。此處有兩個坑,一是變量聲明提高,二是函數表達式。prototype
即全部聲明變量或聲明函數都會被提高到當前函數的頂部。指針
例以下代碼:code
console.log(‘x’ in window);//true var x;
x = 0;
代碼執行時js引擎會將聲明語句提高至代碼最上方,變爲:
var x; console.log(‘x’ in window);//true x = 0;
var getName
與function getName
都是聲明語句,區別在於 var getName
是 函數表達式 ,而 function getName
是 函數聲明 。關於JS中的各類函數建立方式能夠看 大部分人都會作錯的經典JS閉包面試題 這篇文章有詳細說明。
函數表達式最大的問題,在於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函數。
同理,原題中代碼最終執行時的是:
function Foo() { getName = function () { console.log(1); }; return this; } var getName;//只提高變量聲明 function getName() { console.log(5);}//提高函數聲明,覆蓋var的聲明 Foo.getName = function () { console.log(2);}; Foo.prototype.getName = function () { console.log(3);}; getName = function () { console.log(4);};//最終的賦值再次覆蓋function getName聲明 getName();//最終輸出4
第三問的 Foo().getName();
先執行了Foo
函數,而後調用Foo
函數的返回值對象的getName
屬性函數。
Foo
函數的第一句 getName = function () { console.log(1); };
是一句函數賦值語句,注意它沒有var聲明,因此先向當前Foo函數做用域內尋找getName變量,沒有。再向當前函數做用域上層,即外層做用域內尋找是否含有getName變量,找到了,也就是第二問中的alert(4)
函數,將此變量的值賦值爲 function(){alert(1)}
。
此處其實是將外層做用域內的getName
函數修改了。
注意:此處若依然沒有找到會一直向上查找到
window
對象,若window
對象中也沒有getName
屬性,就在window對象中建立一個getName
變量。
以後Foo
函數的返回值是this
,而JS的this
問題博客園中已經有很是多的文章介紹,這裏再也不多說。
簡單的講, this的指向是由所在函數的調用方式決定的 。而此處的直接調用方式,this
指向window
對象。
遂Foo
函數返回的是window
對象,至關於執行 window.getName()
,而window
中的getName已經被修改成alert(1)
,因此最終會輸出1
此處考察了兩個知識點,一個是變量做用域問題,一個是this
指向問題。
直接調用getName
函數,至關於 window.getName()
,由於這個變量已經被Foo
函數執行時修改了,遂結果與第三問相同,爲1
第五問 new Foo.getName();
,此處考察的是js的運算符優先級問題。
js運算符優先級:
參考連接: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
經過查上表能夠得知點.
的優先級高於new
操做,遂至關因而:
new (Foo.getName)();
因此實際上將getName
函數做爲了構造函數來執行,遂彈出2。
第六問 new Foo().getName()
,首先看運算符優先級()
高於new
,實際執行爲
(new Foo()).getName()
遂先執行Foo
函數,而Foo
此時做爲構造函數卻有返回值,因此這裏須要說明下js中的構造函數返回值問題。
在傳統語言中,構造函數不該該有返回值,實際執行的返回值就是此構造函數的實例化對象。
而在js中構造函數能夠有返回值也能夠沒有。
一、沒有返回值則按照其餘語言同樣返回實例化對象。
function F(){} new F() //>F {}
二、如有返回值則檢查其返回值是否爲 引用類型 。若是是非引用類型,如基本類型(string
,number
,boolean
,null
,undefined
)則與無返回值相同,實際返回其實例化對象。
function F(){return 1;} new F() //>F {}
三、若返回值是引用類型,則實際返回值爲這個引用類型。
function F(){return {a:1};} new F() //>Object {a: 1}
原題中,返回的是this
,而this
在構造函數中原本就表明當前實例化對象,遂最終Foo
函數返回實例化對象。
以後調用實例化對象的getName
函數,由於在Foo
構造函數中沒有爲實例化對象添加任何屬性,遂到當前對象的原型對象(prototype
)中尋找getName
,找到了。
遂最終輸出3。
第七問, new new Foo().getName();
一樣是運算符優先級問題。
最終實際執行爲:
new ((new Foo()).getName)();
先初始化Foo
的實例化對象,而後將其原型上的getName
函數做爲構造函數再次new
。
遂最終結果爲3