前端同學經常忽視的一個 JavaScript 面試題

来源:https://www.cnblogs.com/bjy216326/archive/2020/07/06/13255145.html
-Advertisement-
Play Games

題目 function Foo() { getName = function () { alert (1); }; return this; } Foo.getName = function () { alert (2);}; Foo.prototype.getName = function ()  ...


題目

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指針指向、運算符優先順序、原型、繼承、全局變數污染、對象屬性及原型屬性優先順序等知識,此題在網上也有部分相關的解釋,當然我覺得有部分解釋還欠妥,不夠清晰,特地重頭到尾來分析一次,當然我們會把最終答案放在後面,並把此題再改高一點點難度,改進版也放在最後,速食麵試官在出題的時候有個參考,更多詳情可關註本文作者@Wscats

第一問

先看此題的上半部分做了什麼,首先定義了一個叫Foo的函數,之後為Foo創建了一個叫getName的靜態屬性存儲了一個匿名函數,之後為Foo的原型對象新創建了一個叫getName的匿名函數。之後又通過函數變數表達式創建了一個getName的函數,最後再聲明一個叫getName函數。

第一問的Foo.getName自然是訪問Foo函數上存儲的靜態屬性,答案自然是2,這裡就不需要解釋太多的,一般來說第一問對於稍微懂JS基礎的同學來說應該是沒問題的,當然我們可以用下麵的代碼來回顧一下基礎,先加深一下瞭解

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

 

註意下麵這幾點:

  • 調用公有方法,公有屬性,我們必需先實例化對象,也就是用new操作符實化對象,就可構造函數實例化對象的方法和屬性,並且公有方法是不能調用私有方法和靜態方法的
  • 靜態方法和靜態屬性就是我們無需實例化就可以調用
  • 而對象的私有方法和屬性,外部是不可以訪問的

第二問

第二問,直接調用getName函數。既然是直接調用那麼就是訪問當前上文作用域內的叫getName的函數,所以這裡應該直接把關註點放在4和5上,跟1 2 3都沒什麼關係。當然後來我問了我的幾個同事他們大多數回答了5。此處其實有兩個坑,一是變數聲明提升,二是函數表達式和函數聲明的區別。

我們來看看為什麼,可參考(1)關於Javascript的函數聲明和函數表達式 (2)關於JavaScript的變數提升

在Javascript中,定義函數有兩種類型

函數聲明

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

函數表達式

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

 

先看下麵這個經典問題,在一個程式裡面同時用函數聲明和函數表達式定義一個名為getName的函數

getName() //oaoafly
var getName = function() {
console.log('wscat')
}
getName() //wscat
function getName() {
console.log('oaoafly')
}
getName() //wscat

 

上面的代碼看起來很類似,感覺也沒什麼太大差別。但實際上,Javascript函數上的一個“陷阱”就體現在Javascript兩種類型的函數定義上。

  • JavaScript 解釋器中存在一種變數聲明被提升的機制,也就是說函數聲明會被提升到作用域的最前面,即使寫代碼的時候是寫在最後面,也還是會被提升至最前面。
  • 而用函數表達式創建的函數是在運行時進行賦值,且要等到表達式賦值完成後才能調用
var getName //變數被提升,此時為undefined

getName() //oaoafly 函數被提升 這裡受函數聲明的影響,雖然函數聲明在最後可以被提升到最前面了
var getName = function() {
console.log('wscat')
} //函數表達式此時才開始覆蓋函數聲明的定義
getName() //wscat
function getName() {
console.log('oaoafly')
}
getName() //wscat 這裡就執行了函數表達式的值

 

所以可以分解為這兩個簡單的問題來看清楚區別的本質

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')
}

 

這個區別看似微不足道,但在某些情況下確實是一個難以察覺並且“致命“的陷阱。出現這個陷阱的本質原因體現在這兩種類型在函數提升和運行時機(解析時/運行時)上的差異。

當然我們給一個總結:Javascript中函數聲明和函數表達式是存在區別的,函數聲明在JS解析時進行函數提升,因此在同一個作用域內,不管函數聲明在哪裡定義,該函數都可以進行調用。而函數表達式的值是在JS運行時確定,並且在表達式賦值完成後,該函數才能調用。

所以第二問的答案就是4,5的函數聲明被4的函數表達式覆蓋了

第三問

Foo().getName(); 先執行了Foo函數,然後調用Foo函數的返回值對象的getName屬性函數。

Foo函數的第一句getName = function () { alert (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指向問題
我們可以利用下麵代碼來回顧下這兩個知識點

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"); //當然傳參是局部變數,但函數裡面我沒有接受這個參數
console.log(name) //Oaoafly
console.log(getPrivate()) //Stacsw

 

因為JS沒有塊級作用域,但是函數是能產生一個作用域的,函數內部不同定義值的方法會直接或者間接影響到全局或者局部變數,函數內部的私有變數可以用閉包獲取,函數還真的是第一公民呀~

而關於this,this的指向在函數定義的時候是確定不了的,只有函數執行的時候才能確定this到底指向誰,實際上this的最終指向的是那個調用它的對象

所以第三問中實際上就是window在調用**Foo()**函數,所以this的指向是window

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

第四問

直接調用getName函數,相當於window.getName(),因為這個變數已經被Foo函數執行時修改了,遂結果與第三問相同,為1,也就是說Foo執行後把全局的getName函數給重寫了一次,所以結果就是Foo()執行重寫的那個getName函數

第五問

第五問new Foo.getName();此處考察的是JS的運算符優先順序問題,我覺得這是這題靈魂的所在,也是難度比較大的一題

下麵是JS運算符的優先順序表格,從高到低排列。可參考MDN運算符優先順序

 

yield*從右到左yield* …1展開運算符n/a... …0逗號從左到右… , …

這題首先看優先順序的第18和第17都出現關於new的優先順序,new (帶參數列表)比new (無參數列表)高比函數調用高,跟成員訪問同級

new Foo.getName();的優先順序是這樣的

相當於是:

new (Foo.getName)();
  • 點的優先順序(18)比new無參數列表(17)優先順序高
  • 當點運算完後又因為有個括弧(),此時就是變成new有參數列表(18),所以直接執行new,當然也可能有朋友會有疑問為什麼遇到()不函數調用再new呢,那是因為函數調用(17)比new有參數列表(18)優先順序低

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

所以這裡實際上將getName函數作為了構造函數來執行,遂彈出2。

第六問

這一題比上一題的唯一區別就是在Foo那裡多出了一個括弧,這個有括弧跟沒括弧我們在第五問的時候也看出來優先順序是有區別的

(new Foo()).getName()

那這裡又是怎麼判斷的呢?首先new有參數列表(18)跟點的優先順序(18)是同級,同級的話按照從左向右的執行順序,所以先執行new有參數列表(18)再執行點的優先順序(18),最後再函數調用(17)

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

這裡還有一個小知識點,Foo作為構造函數有返回值,所以這裡需要說明下JS中的構造函數返回值問題。

構造函數的返回值

在傳統語言中,構造函數不應該有返回值,實際執行的返回值就是此構造函數的實例化對象。
而在JS中構造函數可以有返回值也可以沒有。

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

 

  1. 若有返回值則檢查其返回值是否為引用類型。如果是非引用類型,如基本類型(String,Number,Boolean,Null,Undefined)則與無返回值相同,實際返回其實例化對象。
function Foo(name) {
this.name = name
return 520
}
console.log(new Foo('wscats'))

 

  1. 若返回值是引用類型,則實際返回值為這個引用類型。
function Foo(name) {
this.name = name
return {
age: 16
}
}
console.log(new Foo('wscats'))

原題中,由於返回的是this,而this在構造函數中本來就代表當前實例化對象,最終Foo函數返回實例化對象。

之後調用實例化對象的getName函數,因為在Foo構造函數中沒有為實例化對象添加任何屬性,當前對象的原型對象(prototype)中尋找getName函數。

當然這裡再拓展個題外話,如果構造函數和原型鏈都有相同的方法,如下麵的代碼,那麼預設會拿構造函數的公有方法而不是原型鏈,這個知識點在原題中沒有表現出來,後面改進版我已經加上。

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();同樣是運算符優先順序問題。做到這一題其實我已經覺得答案沒那麼重要了,關鍵只是考察面試者是否真的知道面試官在考察我們什麼。
最終實際執行為:

new ((new Foo()).getName)();

 

new有參數列表(18)->new有參數列表(18)

先初始化Foo的實例化對象,然後將其原型上的getName函數作為構造函數再次new,所以最終結果為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

後續

後續我把這題的難度再稍微加大一點點(附上答案),在Foo函數裡面加多一個公有方法getName,對於下麵這題如果用在面試題上那通過率可能就更低了,因為難度又大了一點,又多了兩個坑,但是明白了這題的原理就等同於明白了上面所有的知識點了

function Foo() {
this.getName = function() {
console.log(3);
return {
getName: getName //這個就是第六問中涉及的構造函數的返回值問題
}
}; //這個就是第六問中涉及到的,JS構造函數公有方法和原型鏈方法的優先順序
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

 

最後,其實我是不建議把這些題作為考察面試者的唯一評判,但是作為一名合格的前端工程師我們不應該因為浮躁忽略了我們的一些最基本的基礎知識,當然我也祝願所有面試者找到一份理想的工作,祝願所有面試官找到心中那匹千里馬~

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 新聞 Android的電話應用將能夠告訴你為什麼企業要給你打電話 Google Play Store可能會重新開始顯示應用更新通知 谷歌確認將推出新功能 對標蘋果AirDrop 谷歌新版SafetyNet可能會讓root和定製ROM走向終結 Android版Gboard輸入法正測試面向IM應用的自動 ...
  • H分形是由一個字母H演化出迷宮一樣場景的分形圖案,其構造過程是:取一個中心點(x,y),以此中心點繪製一條長為L的水平直線和兩條長為H的豎直直線,構成一個字母“H”的形狀;再以兩條豎直直線的上下共4個端點為中心點,分別繪製一條長為L/2的水平直線和兩條長為H/2的豎直直線;重覆以上操作直至達到要求的 ...
  • 德國數學家David Hilbert在1891年構造了一種曲線,首先把一個正方形等分成四個小正方形,依次從西北角的正方形中心出發往南到西南正方形中心,再往東到東南角的正方形中心,再往北到東北角正方形中心,這是一次迭代;如果對四個小正方形繼續上述過程,往下劃分,反覆進行,最終就得到一條可以填滿整個正方 ...
  • 實現 React Hooks UI 開發有兩個問題: 展示覆用 邏輯復用 展示覆用目前基本使用組件化來解決,邏輯復用一直以來都沒有特別好的解決方案。React 從一開始的 mixin ,到 高階組件 以及 Render Props ,都是在試圖解決這個問題,但是都引入了一些別的問題。 Mixins ...
  • <script type="text/javascript"> function clickIE4() { if (event.button == 2) { return false; } } function clickNS4(e) { if (document.layers || documen ...
  • 最近在教我老婆學習前端,她說想要學習前端,自己在家賺點外快,自己賺點家用。我也拗不過她,而且其實我也挺佩服的。所以就教她了。最近我想考一考她對css中偽類的瞭解,所以就問了她瞭解css多少個偽類,偽類是什麼? 她說css 偽類是用於向某些選擇器添加特殊的效果,是動態的,指當前元素所處的狀態或者特性。 ...
  • 直接複製就能用 wxml <view bindtap="showModal">點這裡</view> <view class="wrap"> <view class="modal modal-bottom-dialog" hidden="{{hideFlag}}"> <view class="moda ...
  • 前言 前幾天,我接到了一個項目,模塊中要寫一個卡券效果,當時沒有圖片,也就是要用css來實現,當時我是懵逼的,也沒有寫過這樣的,一時間不知道怎麼寫,畢竟要寫的像UI設計的一樣美觀。我就只好求救我的大神級別的同事了。不僅css玩的溜,人家JavaScript玩的更溜,閣下實在是佩服。 常見的卡券樣式如 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...