一道經(jīng)典的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);}
 
//請(qǐng)寫出以下輸出結(jié)果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

題目如上,這道題的經(jīng)典之處在于它綜合考察了面試者的JavaScript的綜合能力,包含了變量定義提升、this指針指向、運(yùn)算符優(yōu)先級(jí)、原型、繼承、全局變量污染、對(duì)象屬性及原型屬性優(yōu)先級(jí)等知識(shí),此題在網(wǎng)上也有部分相關(guān)的解釋,當(dāng)然我覺得有部分解釋還欠妥,不夠清晰,特地重頭到尾來(lái)分析一次,當(dāng)然我們會(huì)把最終答案放在后面,并把此題再改高一點(diǎn)點(diǎn)難度,改進(jìn)版也放在最后,方便面試官在出題的時(shí)候有個(gè)參考

第一問

先看此題的上半部分做了什么,首先定義了一個(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ù)。

第一問的Foo.getName自然是訪問Foo函數(shù)上存儲(chǔ)的靜態(tài)屬性,答案自然是2,這里就不需要解釋太多的,一般來(lái)說(shuō)第一問對(duì)于稍微懂JS基礎(chǔ)的同學(xué)來(lái)說(shuō)應(yīng)該是沒問題的,當(dāng)然我們可以用下面的代碼來(lái)回顧一下基礎(chǔ),先加深一下了解

function User(name) {
    var name = name; //私有屬性
    this.name = name; //公有屬性
    function getName() { //私有方法
        return name;
    }
}
User.prototype.getName = function() { //公有方法
    return this.name;
}
User.name = 'Wscats'; //靜態(tài)屬性
User.getName = function() { //靜態(tài)方法
    return this.name;
}
var Wscat = new User('Wscats'); //實(shí)例化

注意下面這幾點(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ì)象的私有方法和屬性,外部是不可以訪問的

第二問

第二問,直接調(diào)用getName函數(shù)。既然是直接調(diào)用那么就是訪問當(dāng)前上文作用域內(nèi)的叫g(shù)etName的函數(shù),所以這里應(yīng)該直接把關(guān)注點(diǎn)放在4和5上,跟1 2 3都沒什么關(guān)系。當(dāng)然后來(lái)我問了我的幾個(gè)同事他們大多數(shù)回答了5。此處其實(shí)有兩個(gè)坑,一是變量聲明提升,二是函數(shù)表達(dá)式和函數(shù)聲明的區(qū)別。
我們來(lái)看看為什么,可參考(1)關(guān)于Javascript的函數(shù)聲明和函數(shù)表達(dá)式 (2)關(guān)于JavaScript的變量提升
在Javascript中,定義函數(shù)有兩種類型

函數(shù)聲明

// 函數(shù)聲明
function wscat(type){
  return type==="wscat";
}

函數(shù)表達(dá)式

// 函數(shù)表達(dá)式
var oaoafly = function(type){
  return type==="oaoafly";
}

先看下面這個(gè)經(jīng)典問題,在一個(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)很類似,感覺也沒什么太大差別。但實(shí)際上,Javascript函數(shù)上的一個(gè)“陷阱”就體現(xiàn)在Javascript兩種類型的函數(shù)定義上。

  • JavaScript 解釋器中存在一種變量聲明被提升的機(jī)制,也就是說(shuō)函數(shù)聲明會(huì)被提升到作用域的最前面,即使寫代碼的時(shí)候是寫在最后面,也還是會(huì)被提升至最前面。
  • 而用函數(shù)表達(dá)式創(chuàng)建的函數(shù)是在運(yùn)行時(shí)進(jìn)行賦值,且要等到表達(dá)式賦值完成后才能調(diào)用
var getName//變量被提升,此時(shí)為undefined
getName()//oaoafly 函數(shù)被提升 這里受函數(shù)聲明的影響,雖然函數(shù)聲明在最后可以被提升到最前面了
var getName = function() {
    console.log('wscat')
}//函數(shù)表達(dá)式此時(shí)才開始覆蓋函數(shù)聲明的定義
getName()//wscat
function getName() {
    console.log('oaoafly')
}
getName()//wscat 這里就執(zhí)行了函數(shù)表達(dá)式的值

所以可以分解為這兩個(gè)簡(jiǎ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')
}
var getName;
console.log(getName)//function getName() {console.log('oaoafly')}
getName()//oaoafly
function getName() {
    console.log('oaoafly')
}

這個(gè)區(qū)別看似微不足道,但在某些情況下確實(shí)是一個(gè)難以察覺并且“致命“的陷阱。出現(xiàn)這個(gè)陷阱的本質(zhì)原因體現(xiàn)在這兩種類型在函數(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)用。
所以第二問的答案就是4,5的函數(shù)聲明被4的函數(shù)表達(dá)式覆蓋了

第三問

Foo().getName();先執(zhí)行了Foo函數(shù),然后調(diào)用Foo函數(shù)的返回值對(duì)象的getName屬性函數(shù)。
Foo函數(shù)的第一句getName = function () { alert (1); };是一句函數(shù)賦值語(yǔ)句,注意它沒有var聲明,所以先向當(dāng)前Foo函數(shù)作用域內(nèi)尋找getName變量,沒有。再向當(dāng)前函數(shù)作用域上層,即外層作用域內(nèi)尋找是否含有g(shù)etName變量,找到了,也就是第二問中的alert(4)函數(shù),將此變量的值賦值為function(){alert(1)}。
此處實(shí)際上是將外層作用域內(nèi)的getName函數(shù)修改了。

注意:此處若依然沒有找到會(huì)一直向上查找到window對(duì)象,若window對(duì)象中也沒有g(shù)etName屬性,就在window對(duì)象中創(chuàng)建一個(gè)getName變量。

之后Foo函數(shù)的返回值是this,而JS的this問題已經(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),一個(gè)是變量作用域問題,一個(gè)是this指向問題
我們可以利用下面代碼來(lái)回顧下這兩個(gè)知識(shí)點(diǎn)

var name = "Wscats";//全局變量
window.name = "Wscats";//全局變量
function getName(name) {
    console.log(name); //Hello
    name = "Oaoafly"; //去掉var變成了全局變量
    var privateName = "Stacsw";
    return function() {
        console.log(this);//window
        return privateName
    }
}
var getPrivate = getName("Hello"); //傳參是局部變量
console.log(name) //Oaoafly
console.log(getPrivate()) //Stacsw

因?yàn)镴S沒有塊級(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ì)象
所以第三問中實(shí)際上就是window在調(diào)用**Foo()**函數(shù),所以this的指向是window

window.Foo().getName();
//->window.getName();

第四問

直接調(diào)用getName函數(shù),相當(dāng)于window.getName(),因?yàn)檫@個(gè)變量已經(jīng)被Foo函數(shù)執(zhí)行時(shí)修改了,遂結(jié)果與第三問相同,為1,也就是說(shuō)Foo執(zhí)行后把全局的getName函數(shù)給重寫了一次,所以結(jié)果就是Foo()執(zhí)行重寫的那個(gè)getName函數(shù)

第五問

第五問new Foo.getName();此處考察的是JS的運(yùn)算符優(yōu)先級(jí)問題,我覺得這是這題靈魂的所在,也是難度比較大的一題
下面是JS運(yùn)算符的優(yōu)先級(jí)表格,從高到低排列??蓞⒖?a target="_blank" rel="nofollow">MDN運(yùn)算符優(yōu)先級(jí)

第五問new Foo.getName();此處考察的是JS的運(yùn)算符優(yōu)先級(jí)問題,我覺得這是這題靈魂的所在,也是難度比較大的一題
下面是JS運(yùn)算符的優(yōu)先級(jí)表格,從高到低排列??蓞⒖?a target="_blank" rel="nofollow">MDN運(yùn)算符優(yōu)先級(jí)

優(yōu)先級(jí)運(yùn)算類型關(guān)聯(lián)性運(yùn)算符19圓括號(hào)n/a( … )18成員訪問從左到右… . …需計(jì)算的成員訪問從左到右… [ … ]new (帶參數(shù)列表)n/a new… ( … )17函數(shù)調(diào)用從左到右… ( … )new (無(wú)參數(shù)列表)從右到左new …16后置遞增(運(yùn)算符在后)n/a… ++后置遞減(運(yùn)算符在后)n/a… --15邏輯非從右到左! …按位非從右到左~ …一元加法從右到左+ …一元減法從右到左- …前置遞增從右到左++ …前置遞減從右到左-- …typeof從右到左typeof …void從右到左void …delete從右到左delete …14乘法從左到右… * …除法從左到右… / …取模從左到右… % …13加法從左到右… + …減法從左到右… - …12按位左移從左到右… << …按位右移從左到右… >> …無(wú)符號(hào)右移從左到右… >>> …11小于從左到右… < …小于等于從左到右… <= …大于從左到右… > …大于等于從左到右… >= …in從左到右… in …instanceof從左到右… instanceof …10等號(hào)從左到右… == …非等號(hào)從左到右… != …全等號(hào)從左到右… === …非全等號(hào)從左到右… !== …9按位與從左到右… & …8按位異或從左到右… ^ …7按位或從左到右… 按位或 …6邏輯與從左到右… && …5邏輯或從左到右… 邏輯或 …4條件運(yùn)算符從右到左… ? … : …3賦值從右到左… = …… += …… -= …… *= …… /= …… %= …… <<= …… >>= …… >>>= …… &= …… ^= …… 或= …2yield從右到左yield …yield*從右到左yield* …1展開運(yùn)算符n/a... …0逗號(hào)從左到右… , …

這題首先看優(yōu)先級(jí)的第18和第17都出現(xiàn)關(guān)于new的優(yōu)先級(jí),new (帶參數(shù)列表)比new (無(wú)參數(shù)列表)高比函數(shù)調(diào)用高,跟成員訪問同級(jí)

new Foo.getName();的優(yōu)先級(jí)是這樣的

相當(dāng)于是:

new (Foo.getName)();
  • 點(diǎn)的優(yōu)先級(jí)(18)比new無(wú)參數(shù)列表(17)優(yōu)先級(jí)高
  • 當(dāng)點(diǎn)運(yùn)算完后又因?yàn)橛袀€(gè)括號(hào)(),此時(shí)就是變成new有參數(shù)列表(18),所以直接執(zhí)行new,當(dāng)然也可能有朋友會(huì)有疑問為什么遇到()不函數(shù)調(diào)用再new呢,那是因?yàn)楹瘮?shù)調(diào)用(17)比new有參數(shù)列表(18)優(yōu)先級(jí)低

.成員訪問(18)->new有參數(shù)列表(18)

所以這里實(shí)際上將getName函數(shù)作為了構(gòu)造函數(shù)來(lái)執(zhí)行,遂彈出2。

第六問

這一題比上一題的唯一區(qū)別就是在Foo那里多出了一個(gè)括號(hào),這個(gè)有括號(hào)跟沒括號(hào)我們?cè)诘谖鍐柕臅r(shí)候也看出來(lái)優(yōu)先級(jí)是有區(qū)別的

(new Foo()).getName()

那這里又是怎么判斷的呢?首先new有參數(shù)列表(18)跟點(diǎn)的優(yōu)先級(jí)(18)是同級(jí),同級(jí)的話按照從左向右的執(zhí)行順序,所以先執(zhí)行new有參數(shù)列表(18)再執(zhí)行點(diǎn)的優(yōu)先級(jí)(18),最后再函數(shù)調(diào)用(17)

new有參數(shù)列表(18)->.成員訪問(18)->()函數(shù)調(diào)用(17)

這里還有一個(gè)小知識(shí)點(diǎn),F(xiàn)oo作為構(gòu)造函數(shù)有返回值,所以這里需要說(shuō)明下JS中的構(gòu)造函數(shù)返回值問題。

構(gòu)造函數(shù)的返回值

在傳統(tǒng)語(yǔ)言中,構(gòu)造函數(shù)不應(yīng)該有返回值,實(shí)際執(zhí)行的返回值就是此構(gòu)造函數(shù)的實(shí)例化對(duì)象。
而在JS中構(gòu)造函數(shù)可以有返回值也可以沒有。

  1. 沒有返回值則按照其他語(yǔ)言一樣返回實(shí)例化對(duì)象。
function Foo(name){
    this.name = name
}
console.log(new Foo('wscats'))

// chrome結(jié)果
foo 
  name: "wscats"
  __proto__: Object
  1. 若有返回值則檢查其返回值是否為引用類型。如果是非引用類型,如基本類型(String,Number,Boolean,Null,Undefined)則與無(wú)返回值相同,實(shí)際返回其實(shí)例化對(duì)象。
function Foo(name){
    this.name = name
    return 520
}
console.log(new Foo('wscats'))

// chrome結(jié)果
foo 
  name: "wscats"
  __proto__: Object
  1. 若返回值是引用類型,則實(shí)際返回值為這個(gè)引用類型。
function Foo(name){
    this.name = name
    return {
        age:16
  }
}
console.log(new Foo('wscats'))

// chrome結(jié)果
Object 
  age: 16
  __proto__: Object

原題中,由于返回的是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ù)中沒有為實(shí)例化對(duì)象添加任何屬性,當(dāng)前對(duì)象的原型對(duì)象(prototype)中尋找getName函數(shù)。
當(dāng)然這里再拓展個(gè)題外話,如果構(gòu)造函數(shù)和原型鏈都有相同的方法,如下面的代碼,那么默認(rèn)會(huì)拿構(gòu)造函數(shù)的公有方法而不是原型鏈,這個(gè)知識(shí)點(diǎn)在原題中沒有表現(xiàn)出來(lái),后面改進(jìn)版我已經(jīng)加上。

function Foo(name) {
    this.name = name
    this.getName = function() {
        return this.name
    }
}
Foo.prototype.name = 'Oaoafly';
Foo.prototype.getName = function() {
    return 'Oaoafly'
}
console.log((new Foo('Wscats')).name)//Wscats
console.log((new Foo('Wscats')).getName())//Wscats

第七問

new new Foo().getName();同樣是運(yùn)算符優(yōu)先級(jí)問題。做到這一題其實(shí)我已經(jīng)覺得答案沒那么重要了,關(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

答案

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();//2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3

后續(xù)

后續(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è)就是第六問中涉及的構(gòu)造函數(shù)的返回值問題
        }
    };//這個(gè)就是第六問中涉及到的,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
                //多了一問
new Foo().getName().getName(); //3 1
new new Foo().getName(); //3

這是我早上在刷GitHub的時(shí)候,看到的關(guān)于一道面試題的分析,非常之經(jīng)典,也非常常見
于是轉(zhuǎn)來(lái)和大家一起學(xué)習(xí)

原文地址:https://github.com/Wscats/Good-text-Share/issues/85
原文的原文地址??:http://www.cnblogs.com/xxcanghai/p/5189353.html

生命不息,折騰不止...
I'm not a real coder, but i love it so much!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 34,623評(píng)論 18 399
  • Github原文 題目 這幾天面試上幾次碰上這道經(jīng)典的題目,特地從頭到尾來(lái)分析一次答案,這道題的經(jīng)典之處在于它綜合...
    秋風(fēng)喵閱讀 948評(píng)論 0 19
  • 最近在網(wǎng)上看了一道有關(guān)前端的面試題感覺很不錯(cuò),就在這里分享給大家。 題目: function Foo() { g...
    穿越人海遇見你閱讀 1,035評(píng)論 0 12
  • 題目如下: 答案是: 此題包含7小問,分別說(shuō)下。 第一問 先看此題的上半部分做了什么,首先定義了一個(gè)叫Foo的函數(shù)...
    風(fēng)起云帆閱讀 329評(píng)論 0 0
  • 我要好好記住,并時(shí)時(shí)提醒! 老太太的輪椅上系個(gè)藍(lán)色小吊牌,上面寫著"I'm not yours",本來(lái)是代表輪椅的...
    蔥伴侶閱讀 194評(píng)論 0 0

友情鏈接更多精彩內(nèi)容