題目 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);} //請(qǐng)寫(xiě)出以下輸出結(jié)果: Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName(); 這幾天面試上幾次碰上這道經(jīng)典的題目,,特地從頭到尾來(lái)分析一次答案,。 這道題的經(jīng)典之處在于它綜合考察了面試者的JavaScript的綜合能力,包含了變量定義提升,、this指針指向,、運(yùn)算符優(yōu)先級(jí)、原型,、繼承,、全局變量污染、對(duì)象屬性及原型屬性?xún)?yōu)先級(jí)等知識(shí),。 此題在網(wǎng)上也有部分相關(guān)的解釋?zhuān)?dāng)然我覺(jué)得有部分解釋還欠妥,,不夠清晰,特地重頭到尾來(lái)分析一次,。 當(dāng)然我們會(huì)把最終答案放在后面,,并把此題再改高一點(diǎn)點(diǎn)難度,改進(jìn)版也放在最后,,方便面試官在出題的時(shí)候有個(gè)參考,。 問(wèn)題 第一問(wèn) 先看此題的上半部分做了什么。 首先定義了一個(gè)叫Foo的函數(shù),,之后為Foo創(chuàng)建了一個(gè)叫g(shù)etName的靜態(tài)屬性存儲(chǔ)了一個(gè)匿名函數(shù),,之后為Foo的原型對(duì)象新創(chuàng)建了一個(gè)叫g(shù)etName的匿名函數(shù)。之后又通過(guò)函數(shù)變量表達(dá)式創(chuàng)建了一個(gè)getName的函數(shù),,最后再聲明一個(gè)叫g(shù)etName函數(shù),。 第一問(wèn)的Foo.getName自然是訪問(wèn)Foo函數(shù)上存儲(chǔ)的靜態(tài)屬性,答案自然是2,。 這里就不需要解釋太多的,,一般來(lái)說(shuō)第一問(wèn)對(duì)于稍微懂JS基礎(chǔ)的同學(xué)來(lái)說(shuō)應(yīng)該是沒(méi)問(wèn)題的,當(dāng)然我們可以用下面的代碼來(lái)回顧一下基礎(chǔ),先加深一下了解,。
注意下面這幾點(diǎn) 調(diào)用公有方法,,公有屬性,我們必需先實(shí)例化對(duì)象,,也就是用new操作符實(shí)化對(duì)象,,就可構(gòu)造函數(shù)實(shí)例化對(duì)象的方法和屬性,并且公有方法是不能調(diào)用私有方法和靜態(tài)方法的,。 靜態(tài)方法和靜態(tài)屬性就是我們無(wú)需實(shí)例化就可以調(diào)用,,而對(duì)象的私有方法和屬性,外部是不可以訪問(wèn)的。 第二問(wèn) 第二問(wèn),,直接調(diào)用getName函數(shù),。 既然是直接調(diào)用那么就是訪問(wèn)當(dāng)前上文作用域內(nèi)的叫g(shù)etName的函數(shù),所以這里應(yīng)該直接把關(guān)注點(diǎn)放在4和5上,,跟1 2 3都沒(méi)什么關(guān)系,。 當(dāng)然后來(lái)我問(wèn)了我的幾個(gè)同事他們大多數(shù)回答了5,。此處其實(shí)有兩個(gè)坑,一是變量聲明提升,,二是函數(shù)表達(dá)式和函數(shù)聲明的區(qū)別,。 我們來(lái)看看為什么,可參考
在Javascript中,,定義函數(shù)有兩種類(lèi)型 函數(shù)聲明 // 函數(shù)聲明 function wscat(type) { return type === 'wscat'; } 函數(shù)表達(dá)式
先看下面這個(gè)經(jīng)典問(wèn)題,,在一個(gè)程序里面同時(shí)用函數(shù)聲明和函數(shù)表達(dá)式定義一個(gè)名為getName的函數(shù)。 getName() //oaoafly var getName = function() { console.log('wscat') } getName() //wscat function getName() { console.log('oaoafly') } getName() //wscat 上面的代碼看起來(lái)很類(lèi)似,,感覺(jué)也沒(méi)什么太大差別,。但實(shí)際上,Javascript函數(shù)上的一個(gè)“陷阱”就體現(xiàn)在Javascript兩種類(lèi)型的函數(shù)定義上,。
所以可以分解為這兩個(gè)簡(jiǎn)單的問(wèn)題來(lái)看清楚區(qū)別的本質(zhì),。 var getName; console.log(getName) //undefined getName() //Uncaught TypeError: getName is not a function var getName = function() { console.log('wscat') }
這個(gè)區(qū)別看似微不足道,但在某些情況下確實(shí)是一個(gè)難以察覺(jué)并且“致命“的陷阱,。 出現(xiàn)這個(gè)陷阱的本質(zhì)原因體現(xiàn)在這兩種類(lèi)型在函數(shù)提升和運(yùn)行時(shí)機(jī)(解析時(shí)/運(yùn)行時(shí))上的差異,。 當(dāng)然我們給一個(gè)總結(jié) Javascript中函數(shù)聲明和函數(shù)表達(dá)式是存在區(qū)別的,函數(shù)聲明在JS解析時(shí)進(jìn)行函數(shù)提升,,因此在同一個(gè)作用域內(nèi),,不管函數(shù)聲明在哪里定義,該函數(shù)都可以進(jìn)行調(diào)用,。 而函數(shù)表達(dá)式的值是在JS運(yùn)行時(shí)確定,,并且在表達(dá)式賦值完成后,該函數(shù)才能調(diào)用,。 所以第二問(wèn)的答案就是4,,5的函數(shù)聲明被4的函數(shù)表達(dá)式覆蓋了。 第三問(wèn) Foo().getName(); 先執(zhí)行了Foo函數(shù),,然后調(diào)用Foo函數(shù)的返回值對(duì)象的getName屬性函數(shù),。 Foo函數(shù)的第一句getName = function () { alert (1); };是一句函數(shù)賦值語(yǔ)句,注意它沒(méi)有var聲明,。 所以先向當(dāng)前Foo函數(shù)作用域內(nèi)尋找getName變量,,沒(méi)有,。再向當(dāng)前函數(shù)作用域上層,即外層作用域內(nèi)尋找是否含有g(shù)etName變量,,找到了,,也就是第二問(wèn)中的alert(4)函數(shù),將此變量的值賦值為function(){alert(1)},。 此處實(shí)際上是將外層作用域內(nèi)的getName函數(shù)修改了,。 注意:此處若依然沒(méi)有找到會(huì)一直向上查找到window對(duì)象,若window對(duì)象中也沒(méi)有g(shù)etName屬性,,就在window對(duì)象中創(chuàng)建一個(gè)getName變量,。 之后Foo函數(shù)的返回值是this,而JS的this問(wèn)題已經(jīng)有非常多的文章介紹,,這里不再多說(shuō),。 簡(jiǎn)單的講,this的指向是由所在函數(shù)的調(diào)用方式?jīng)Q定的,。而此處的直接調(diào)用方式,,this指向window對(duì)象。 遂Foo函數(shù)返回的是window對(duì)象,,相當(dāng)于執(zhí)行window.getName(),,而window中的getName已經(jīng)被修改為alert(1),所以最終會(huì)輸出1 此處考察了兩個(gè)知識(shí)點(diǎn)
我們可以利用下面代碼來(lái)回顧下這兩個(gè)知識(shí)點(diǎn) var name = 'Wscats'; //全局變量 window.name = 'Wscats'; //全局變量 function getName() { name = 'Oaoafly'; //去掉var變成了全局變量 var privateName = 'Stacsw'; return function() { console.log(this); //window return privateName } } var getPrivate = getName('Hello'); //當(dāng)然傳參是局部變量,,但函數(shù)里面我沒(méi)有接受這個(gè)參數(shù) console.log(name) //Oaoafly console.log(getPrivate()) //Stacsw 因?yàn)镴S沒(méi)有塊級(jí)作用域,,但是函數(shù)是能產(chǎn)生一個(gè)作用域的,函數(shù)內(nèi)部不同定義值的方法會(huì)直接或者間接影響到全局或者局部變量,,函數(shù)內(nèi)部的私有變量可以用閉包獲取,,函數(shù)還真的是第一公民呀~ 而關(guān)于this,this的指向在函數(shù)定義的時(shí)候是確定不了的,,只有函數(shù)執(zhí)行的時(shí)候才能確定this到底指向誰(shuí),,實(shí)際上this的最終指向的是那個(gè)調(diào)用它的對(duì)象 所以第三問(wèn)中實(shí)際上就是window在調(diào)用**Foo()**函數(shù),所以this的指向是window
第四問(wèn) 直接調(diào)用getName函數(shù),,相當(dāng)于window.getName(),,因?yàn)檫@個(gè)變量已經(jīng)被Foo函數(shù)執(zhí)行時(shí)修改了,遂結(jié)果與第三問(wèn)相同,,為1,。 也就是說(shuō)Foo執(zhí)行后把全局的getName函數(shù)給重寫(xiě)了一次,所以結(jié)果就是Foo()執(zhí)行重寫(xiě)的那個(gè)getName函數(shù),。 第五問(wèn) new Foo.getName();此處考察的是JS的運(yùn)算符優(yōu)先級(jí)問(wèn)題,,我覺(jué)得這是這題靈魂的所在,也是難度比較大的一題,。 下面是JS運(yùn)算符的優(yōu)先級(jí)表格,,從高到低排列,。 可參考MDN運(yùn)算符優(yōu)先級(jí)
這題首先看優(yōu)先級(jí)的第18和第17都出現(xiàn)關(guān)于new的優(yōu)先級(jí),,new (帶參數(shù)列表)比new (無(wú)參數(shù)列表)高比函數(shù)調(diào)用高,,跟成員訪問(wèn)同級(jí) new Foo.getName();的優(yōu)先級(jí)是這樣的 相當(dāng)于是: new (Foo.getName)();
.成員訪問(wèn)(18)->new有參數(shù)列表(18) 所以這里實(shí)際上將getName函數(shù)作為了構(gòu)造函數(shù)來(lái)執(zhí)行,遂彈出2,。 第六問(wèn) 這一題比上一題的唯一區(qū)別就是在Foo那里多出了一個(gè)括號(hào),,這個(gè)有括號(hào)跟沒(méi)括號(hào)我們?cè)诘谖鍐?wèn)的時(shí)候也看出來(lái)優(yōu)先級(jí)是有區(qū)別的
那這里又是怎么判斷的呢? 首先new有參數(shù)列表(18)跟點(diǎn)的優(yōu)先級(jí)(18)是同級(jí),,同級(jí)的話(huà)按照從左向右的執(zhí)行順序,,所以先執(zhí)行new有參數(shù)列表(18)再執(zhí)行點(diǎn)的優(yōu)先級(jí)(18),最后再函數(shù)調(diào)用(17) new有參數(shù)列表(18)->.成員訪問(wèn)(18)->()函數(shù)調(diào)用(17) 這里還有一個(gè)小知識(shí)點(diǎn),,F(xiàn)oo作為構(gòu)造函數(shù)有返回值,,所以這里需要說(shuō)明下JS中的構(gòu)造函數(shù)返回值問(wèn)題。 構(gòu)造函數(shù)的返回值 在傳統(tǒng)語(yǔ)言中,,構(gòu)造函數(shù)不應(yīng)該有返回值,,實(shí)際執(zhí)行的返回值就是此構(gòu)造函數(shù)的實(shí)例化對(duì)象。而在JS中構(gòu)造函數(shù)可以有返回值也可以沒(méi)有,。 1. 沒(méi)有返回值則按照其他語(yǔ)言一樣返回實(shí)例化對(duì)象,。 function Foo(name) { this.name = name } console.log(new Foo('wscats')) 2. 若有返回值則檢查其返回值是否為引用類(lèi)型。 如果是非引用類(lèi)型,,如基本類(lèi)型(String,Number,Boolean,Null,Undefined)則與無(wú)返回值相同,,實(shí)際返回其實(shí)例化對(duì)象,。
3. 若返回值是引用類(lèi)型,,則實(shí)際返回值為這個(gè)引用類(lèi)型。 function Foo(name) { this.name = name return { age: 16 } } console.log(new Foo('wscats')) 原題中,,由于返回的是this,,而this在構(gòu)造函數(shù)中本來(lái)就代表當(dāng)前實(shí)例化對(duì)象,最終Foo函數(shù)返回實(shí)例化對(duì)象,。 之后調(diào)用實(shí)例化對(duì)象的getName函數(shù),,因?yàn)樵贔oo構(gòu)造函數(shù)中沒(méi)有為實(shí)例化對(duì)象添加任何屬性,當(dāng)前對(duì)象的原型對(duì)象(prototype)中尋找getName函數(shù),。 當(dāng)然這里再拓展個(gè)題外話(huà),,如果構(gòu)造函數(shù)和原型鏈都有相同的方法,如下面的代碼,,那么默認(rèn)會(huì)拿構(gòu)造函數(shù)的公有方法而不是原型鏈,,這個(gè)知識(shí)點(diǎn)在原題中沒(méi)有表現(xiàn)出來(lái),,后面改進(jìn)版我已經(jīng)加上。
第七問(wèn) new new Foo().getName();同樣是運(yùn)算符優(yōu)先級(jí)問(wèn)題,。 做到這一題其實(shí)我已經(jīng)覺(jué)得答案沒(méi)那么重要了,,關(guān)鍵只是考察面試者是否真的知道面試官在考察我們什么。 最終實(shí)際執(zhí)行為: new ((new Foo()).getName)(); new有參數(shù)列表(18)->new有參數(shù)列表(18) 先初始化Foo的實(shí)例化對(duì)象,,然后將其原型上的getName函數(shù)作為構(gòu)造函數(shù)再次new,,所以最終結(jié)果為3 答案
后續(xù) 我把這題的難度再稍微加大一點(diǎn)點(diǎn)(附上答案),在Foo函數(shù)里面加多一個(gè)公有方法getName,,對(duì)于下面這題如果用在面試題上那通過(guò)率可能就更低了,,因?yàn)殡y度又大了一點(diǎn),又多了兩個(gè)坑,,但是明白了這題的原理就等同于明白了上面所有的知識(shí)點(diǎn)了,。 function Foo() { this.getName = function() { console.log(3); return { getName: getName //這個(gè)就是第六問(wèn)中涉及的構(gòu)造函數(shù)的返回值問(wèn)題 } }; //這個(gè)就是第六問(wèn)中涉及到的,JS構(gòu)造函數(shù)公有方法和原型鏈方法的優(yōu)先級(jí) getName = function() { console.log(1); }; return this } Foo.getName = function() { console.log(2); }; Foo.prototype.getName = function() { console.log(6); }; var getName = function() { console.log(4); };
function getName() { console.log(5); } //答案: Foo.getName(); //2 getName(); //4 console.log(Foo()) Foo().getName(); //1 getName(); //1 new Foo.getName(); //2 new Foo().getName(); //3 //多了一問(wèn) new Foo().getName().getName(); //3 1 new new Foo().getName(); //3 最后,,其實(shí)我是不建議把這些題作為考察面試者的唯一評(píng)判,,但是作為一名合格的前端工程師我們不應(yīng)該因?yàn)楦≡旰雎粤宋覀兊囊恍┳罨镜幕A(chǔ)知識(shí)。 當(dāng)然我也祝愿所有面試者找到一份理想的工作,,祝愿所有面試官找到心中那匹千里馬~ |
|