1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        前端同學(xué)經(jīng)常忽視的一個 JavaScript 面試題

        共 8075字,需瀏覽 17分鐘

         ·

        2021-02-01 23:48

        作者 |??Wscats

        來源 | https://github.com/Wscats/articles/issues/85

        題目

        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);}
        //請寫出以下輸出結(jié)果:Foo.getName();getName();Foo().getName();getName();new Foo.getName();new Foo().getName();new new Foo().getName();
        這幾天面試上幾次碰上這道經(jīng)典的題目,特地從頭到尾來分析一次答案,這道題的經(jīng)典之處在于它綜合考察了面試者的JavaScript的綜合能力,包含了變量定義提升、this指針指向、運算符優(yōu)先級、原型、繼承、全局變量污染、對象屬性及原型屬性優(yōu)先級等知識,此題在網(wǎng)上也有部分相關(guān)的解釋,當(dāng)然我覺得有部分解釋還欠妥,不夠清晰,特地重頭到尾來分析一次,當(dāng)然我們會把最終答案放在后面,并把此題再改高一點點難度,改進版也放在最后,方便面試官在出題的時候有個參考,更多詳情可關(guān)注本文作者@Wscats

        第一問

        先看此題的上半部分做了什么,首先定義了一個叫Foo的函數(shù),之后為Foo創(chuàng)建了一個叫g(shù)etName的靜態(tài)屬性存儲了一個匿名函數(shù),之后為Foo的原型對象新創(chuàng)建了一個叫g(shù)etName的匿名函數(shù)。之后又通過函數(shù)變量表達式創(chuàng)建了一個getName的函數(shù),最后再聲明一個叫g(shù)etName函數(shù)。

        第一問的Foo.getName自然是訪問Foo函數(shù)上存儲的靜態(tài)屬性,答案自然是2,這里就不需要解釋太多的,一般來說第一問對于稍微懂JS基礎(chǔ)的同學(xué)來說應(yīng)該是沒問題的,當(dāng)然我們可以用下面的代碼來回顧一下基礎(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'); //實例化

        注意下面這幾點:

        • 調(diào)用公有方法,公有屬性,我們必需先實例化對象,也就是用new操作符實化對象,就可構(gòu)造函數(shù)實例化對象的方法和屬性,并且公有方法是不能調(diào)用私有方法和靜態(tài)方法的

        • 靜態(tài)方法和靜態(tài)屬性就是我們無需實例化就可以調(diào)用

        • 而對象的私有方法和屬性,外部是不可以訪問的

        第二問

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

        我們來看看為什么,可參考(1)關(guān)于Javascript的函數(shù)聲明和函數(shù)表達式?(2)關(guān)于JavaScript的變量提升

        在Javascript中,定義函數(shù)有兩種類型

        函數(shù)聲明

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

        函數(shù)表達式

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

        先看下面這個經(jīng)典問題,在一個程序里面同時用函數(shù)聲明和函數(shù)表達式定義一個名為getName的函數(shù)。

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

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

        • JavaScript 解釋器中存在一種變量聲明被提升的機制,也就是說函數(shù)聲明會被提升到作用域的最前面,即使寫代碼的時候是寫在最后面,也還是會被提升至最前面。

        • 而用函數(shù)表達式創(chuàng)建的函數(shù)是在運行時進行賦值,且要等到表達式賦值完成后才能調(diào)用

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

        所以可以分解為這兩個簡單的問題來看清楚區(qū)別的本質(zhì)

        var getName;console.log(getName) //undefinedgetName() //Uncaught TypeError: getName is not a functionvar getName = function() {  console.log('wscat')}var getName;console.log(getName) //function getName() {console.log('oaoafly')}getName() //oaoaflyfunction getName() {  console.log('oaoafly')}

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

        當(dāng)然我們給一個總結(jié):Javascript中函數(shù)聲明函數(shù)表達式是存在區(qū)別的,函數(shù)聲明在JS解析時進行函數(shù)提升,因此在同一個作用域內(nèi),不管函數(shù)聲明在哪里定義,該函數(shù)都可以進行調(diào)用。而函數(shù)表達式的值是在JS運行時確定,并且在表達式賦值完成后,該函數(shù)才能調(diào)用。

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

        第三問

        Foo().getName();?先執(zhí)行了Foo函數(shù),然后調(diào)用Foo函數(shù)的返回值對象的getName屬性函數(shù)。

        Foo函數(shù)的第一句getName = function () { alert (1); };是一句函數(shù)賦值語句,注意它沒有var聲明,所以先向當(dāng)前Foo函數(shù)作用域內(nèi)尋找getName變量,沒有。再向當(dāng)前函數(shù)作用域上層,即外層作用域內(nèi)尋找是否含有g(shù)etName變量,找到了,也就是第二問中的alert(4)函數(shù),將此變量的值賦值為function(){alert(1)}。

        此處實際上是將外層作用域內(nèi)的getName函數(shù)修改了。

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

        之后Foo函數(shù)的返回值是this,而JS的this問題已經(jīng)有非常多的文章介紹,這里不再多說。

        簡單的講,this的指向是由所在函數(shù)的調(diào)用方式?jīng)Q定的。而此處的直接調(diào)用方式,this指向window對象。

        遂Foo函數(shù)返回的是window對象,相當(dāng)于執(zhí)行window.getName(),而window中的getName已經(jīng)被修改為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"); //當(dāng)然傳參是局部變量,但函數(shù)里面我沒有接受這個參數(shù)console.log(name) //Oaoaflyconsole.log(getPrivate()) //Stacsw

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

        而關(guān)于this,this的指向在函數(shù)定義的時候是確定不了的,只有函數(shù)執(zhí)行的時候才能確定this到底指向誰,實際上this的最終指向的是那個調(diào)用它的對象

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

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

        第四問

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

        第五問

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

        下面是JS運算符的優(yōu)先級表格,從高到低排列??蓞⒖糓DN運算符優(yōu)先級

        優(yōu)先級運算類型關(guān)聯(lián)性運算符
        19圓括號n/a( … )
        18成員訪問從左到右… . …

        需計算的成員訪問從左到右… [ … ]

        new (帶參數(shù)列表)n/a new… ( … )
        17函數(shù)調(diào)用從左到右… ( … )

        new (無參數(shù)列表)從右到左new …
        16后置遞增(運算符在后)n/a… ++

        后置遞減(運算符在后)n/a… --
        15邏輯非從右到左! …

        按位非從右到左~ …

        一元加法從右到左+ …

        一元減法從右到左- …

        前置遞增從右到左++ …

        前置遞減從右到左-- …

        typeof從右到左typeof …

        void從右到左void …

        delete從右到左delete …
        14乘法從左到右… * …

        除法從左到右… / …

        取模從左到右… % …
        13加法從左到右… + …

        減法從左到右… - …
        12按位左移從左到右… << …

        按位右移從左到右… >> …

        無符號右移從左到右… >>> …
        11小于從左到右… < …

        小于等于從左到右… <= …

        大于從左到右… > …

        大于等于從左到右… >= …

        in從左到右… in …

        instanceof從左到右… instanceof …
        10等號從左到右… == …

        非等號從左到右… != …

        全等號從左到右… === …

        非全等號從左到右… !== …
        9按位與從左到右… & …
        8按位異或從左到右… ^ …
        7按位或從左到右… 按位或 …
        6邏輯與從左到右… && …
        5邏輯或從左到右… 邏輯或 …
        4條件運算符從右到左… ? … : …
        3賦值從右到左… = …



        … += …



        … -= …



        … *= …



        … /= …



        … %= …



        … <<= …



        … >>= …



        … >>>= …



        … &= …



        … ^= …



        … 或= …
        2yield從右到左yield …

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

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

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

        相當(dāng)于是:

        new (Foo.getName)();
        • 點的優(yōu)先級(18)比new無參數(shù)列表(17)優(yōu)先級高

        • 當(dāng)點運算完后又因為有個括號(),此時就是變成new有參數(shù)列表(18),所以直接執(zhí)行new,當(dāng)然也可能有朋友會有疑問為什么遇到()不函數(shù)調(diào)用再new呢,那是因為函數(shù)調(diào)用(17)比new有參數(shù)列表(18)優(yōu)先級低

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

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

        第六問

        這一題比上一題的唯一區(qū)別就是在Foo那里多出了一個括號,這個有括號跟沒括號我們在第五問的時候也看出來優(yōu)先級是有區(qū)別的

        (new Foo()).getName()

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

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

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

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

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

        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在構(gòu)造函數(shù)中本來就代表當(dāng)前實例化對象,最終Foo函數(shù)返回實例化對象。

        之后調(diào)用實例化對象的getName函數(shù),因為在Foo構(gòu)造函數(shù)中沒有為實例化對象添加任何屬性,當(dāng)前對象的原型對象(prototype)中尋找getName函數(shù)。

        當(dāng)然這里再拓展個題外話,如果構(gòu)造函數(shù)和原型鏈都有相同的方法,如下面的代碼,那么默認會拿構(gòu)造函數(shù)的公有方法而不是原型鏈,這個知識點在原題中沒有表現(xià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) //Wscatsconsole.log((new Foo('Wscats')).getName()) //Wscats

        第七問

        new new Foo().getName();同樣是運算符優(yōu)先級問題。做到這一題其實我已經(jīng)覺得答案沒那么重要了,關(guān)鍵只是考察面試者是否真的知道面試官在考察我們什么。
        最終實際執(zhí)行為:

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

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

        先初始化Foo的實例化對象,然后將其原型上的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();//2getName();//4Foo().getName();//1getName();//1new Foo.getName();//2new Foo().getName();//3new new Foo().getName();//3

        后續(xù)

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

        function Foo() {  this.getName = function() {    console.log(3);    return {      getName: getName //這個就是第六問中涉及的構(gòu)造函數(shù)的返回值問題    }  }; //這個就是第六問中涉及到的,JS構(gòu)造函數(shù)公有方法和原型鏈方法的優(yōu)先級  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(); //2getName(); //4console.log(Foo())Foo().getName(); //1getName(); //1new Foo.getName(); //2new Foo().getName(); //3//多了一問new Foo().getName().getName(); //3 1new new Foo().getName(); //3


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

        本文完?

        瀏覽 50
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            长泽梓黑人初解禁bdd07 | 偷拍自拍区 | 国产伦精一区二区三区四区 | 草草影院第一页yycc.com | 偷偷解开女同桌的内裤摸网站 | 九哥操逼视频网 | 男人猛躁女人秘 91网站 | 女班长主动给我她的手机号码 | 在线观看无码毛片 | 国产黄色片视频 |