前端常見面試題


1.關於防抖/節流

題目要求很明確,要求最終輸出“searchText1”,並且在500ms后輸出“searchText7”,很明顯就是關於同一個函數在短時間內重復調用,如何限制其調用頻率的功能實現。

 1     function throttle(action, threshold) {
 2         // TODO:
 3     }
 4 
 5     let timer = true;
 6     const triggerScroll = throttle((val) => {
 7         console.log(val);
 8     }, 300);
 9 
10     triggerScroll("searchText1");
11     triggerScroll("searchText2");
12     triggerScroll("searchText3");
13     triggerScroll("searchText4");
14     triggerScroll("searchText5");
15     triggerScroll("searchText6");
16 
17     setTimeout(() => {}, 500);
18 
19     // 期望輸出結果:
20     // searchText1
21     // 500ms 后輸出 searchText7

首先,拿到題目后我們先運行一下,運行后報錯,顯示“triggerScroll is not a function”;由於題目中throttle函數沒有返回值,所以triggerScroll接收到的throttle返回值為undefined,首先我們要在throttle函數中return一個函數,也就是throttle在調用時傳進去的第一個參數。

1  function throttle(action, threshold) {
2     return action
3  }

這時再運行,控制台打印“searchText1-6”;這時我們只需要把防抖的通用模板套進去就可以實現了,我這里用的防抖,其實使用節流函數也是可以實現的,原理相同,我這里就不展示了,完整代碼如下:

 1     function throttle(action, threshold) {
 2         return action
 3     }
 4 
 5     let timer = true;
 6     const triggerScroll = throttle((val) => {
 7         if (timer) {
 8             timer = false;
 9             setTimeout(() => {
10                 timer = true;
11                 console.log(val);
12             }, 300)
13         } else {
14             return false;
15         }
16     }, 300);
17 
18     triggerScroll("searchText1");
19     triggerScroll("searchText2");
20     triggerScroll("searchText3");
21     triggerScroll("searchText4");
22     triggerScroll("searchText5");
23     triggerScroll("searchText6");
24 
25     setTimeout(() => {
26         triggerScroll("searchText7");
27     }, 500);

另附上防抖和節流的通用模板:

//防抖
let timeid = null;
function debounce() {
    if (timeid) {
        clearTimeout(timeid);
    }
    timeid = setTimeout(() => {
        //這里寫操作
        console.log(Math.random());
    }, 500);

}
window.onscroll = debounce;//窗口滾動事件防抖
//節流
let timeid = null;
function debounce(func, wait = 0) {
    if (!timeid) {
        timeid = setTimeout(() => {
            console.log(Math.random());
            timeid = null;
        }, 500);
    }
}
window.onscroll = debounce;

 2.js預編譯以及運行流程

最終輸出結果為“1 1 2 0”。這一題也會涉及到閉包,作用域等知識點。

 1 var i = 0;
 2 function fn() {
 3     i = 1;
 4     console.log(i);
 5     var i;
 6     return function () {
 7         console.log(i);
 8         i += 1;
 9     }
10 }
11 
12 var fun = fn();//1
13 fun();//1
14 fun();//2
15 console.log(i);//0

首先我們需要了解js運行流程:首先進行語法解析;先通篇的檢查js語法,若語法有誤,則報錯。其實進行預編譯;即發生在代碼執行之前,為代碼的執行做的准備工作,一般表現於變量提升,即把變量的聲明提前,但是賦值還是在響應位置賦值,再者就是把整個函數聲明提前。最后js運行流程進入最后一步即解析執行。

搞懂js運行流程再區分作用域,即全局作用域和函數作用域,全局作用域顧名思義就是全局聲明的變量或函數等。函數作用域也就是函數內部的作用域,在函數內部中聲明的變量或函數是不會傳到全局作用域的。

搞懂這兩個概念這一題就能夠迎刃而解了,首先全局聲明了一個變量i並賦值為0,在聲明一個fun接受fn執行后的返回值,所以fn會執行一次,局部變量i在fn函數中重新聲明,變量提升,這時函數fn中使用的i都為函數作用域中聲明的i,並且重新賦值為1,輸出結果1。這里不會影響到全局變量i的值。再觀望整體代碼,全局變量i未被重新賦值,所以最后一行i輸出為0.

再往下走,fu函數的返回值也是一個函數,所以fun接收到的是一個函數,fun函數被執行兩次,注意到在fn函數中,i被重新聲明過,所以這里的返回值函數即fun函數的作用域為局部作用域,使用的變量i為局部變量,即再fn函數中聲明的i,所以輸出1。輸出之后i自增1,即 i = i+1,局部作用域i的值在fun函數弟一次執行后變為2,再次執行后,輸出i為2。


3.值類型和引用類型

這一題主要涉及到值類型和引用類型的存儲方式,即堆內存和棧內存。

1 function fn(obj) {
2     obj.name = "name";
3     obj = new Object();
4     obj.name = "newName";
5 }
6 var xt = new Object();
7 fn(xt);
8 console.log(xt.name);//name

首先題目中聲明了一個函數fn和一個對象xt,調用fn函數,並把xt傳入函數。這是第二行的obj和xt都為引用類型,新增了obj的屬性name值也為name。由於此時obj和xt指向的同一片內存空間,所以xt也會新增name屬性,並設置值為name。故最后一個輸出name。

在第三行中可以看到obj被重新賦值,即重新開辟了一片內存空間,這時obj依然為新增name屬性而不是修改name屬性,並將name屬性設置為newName,所以這是obj新增的屬性不會影響到對象xt的屬性。


4.同步和異步

這一題主要是考察異步代碼和同步代碼執行的先后順序以及js任務隊列。

 1    for (let index = 0; index < 5; index++) {
 2         setTimeout(() => {
 3             console.log(index);
 4         }, 200);
 5     }
 6 
 7     for (let j = 0; j < 5; j++) {
 8         setTimeout(() => {
 9             console.log(j);
10         }, 200);
11     }

首先可以看到題目中有兩個循環體,兩個循環體中分別有兩個延時200ms的延時器。做題之前我們應該先了解到JavaScript對於同步代碼和異步代碼的處理方式,js會先執行同步代碼,遇到異步代碼(例:延時器、計時器、Promise等)會先放置任務隊列,等待觸發條件依次執行。這個概念了解清楚后這題就能夠迎刃而解了。

首先兩個循環體為同步代碼,會先執行。此時第一個循環體會將循環出的五個延時器放置任務隊列,等待200ms后執行。第二個循環體也是如此,所以輸出為“0 1 2 3 4  0 1 2 3 4”。


5.宏任務和微任務

此題主要是考察對於同步異步,以及宏任務和微任務的理解。

//寫出執行結果
setTimeout(() => {
    console.log(1);
}, 0);

new Promise((res) => {
    console.log(2);
    for (var index = 0; index < 1000; index++) {
        index = 9999 && res()

    }
    console.log(3);
}).then(() => {
    console.log(4);
})
console.log(5);

此題最終輸出結果為“2 3 5 4 1”;

解答此題我們需要先了解到Promise的特性。Promise是異步編程的一種解決方案,是解決恐怖回調/回調噩夢的一種方案,其就相當於一個容器,里面保存着某個未來才會結束的事件(通常是一個異步操作)的結果。

創建promise對象,function就相當於容器,用於存放異步代碼(不僅限於異步,同步也可以),此函數在頁面加載時會正常執行,同步代碼會進入主線程執行,異步代碼會進入任務隊列,未來才會得到結果。並且Promise有三種狀態,進行時(pending默認值),已成功(fulfilled/resolved),已失敗(rejected)一旦狀態改變,就不會再變,任何時候都可以得到這個結果,成功后會調用.then()方法,then方法接受兩個參數,皆為回調函數,第一個函數promise狀態變為已成功時觸發的,第二個函數promise狀態變為已失敗時觸發。

了解了Promise后,我們還需要搞清楚宏任務和微任務的概念。在上面我們提到過,js會見異步代碼放置任務隊列,宏任務是由宿主(Node、瀏覽器)發起的,常見的就是計時器和延時器;而微任務是由js引擎發起的,常見的就時Promise;執行順序為微任務先執行,宏任務后執行。

這時我們就可以來解答這個題目了,首先遇到延時器,為宏任務,放入任務隊列,其實遇到promise,先執行promise的同步代碼,輸出2,再進入循環,第一次進入循環index被重新賦值為9999,並且將promise的狀態改為已成功,再往下走輸出3;

此時promise狀態為已成功,觸發then方法的第一個函數,但是promise響應為異步響應。並且為微任務,放置任務隊列,排在宏任務之前。再往下走就輸出5。

此時同步代碼執行完畢,開始執行異步代碼,先執行微任務,輸出4,在執行延時器代碼。輸出5。


好啦。以上就是小譚今天分享的內容啦,也歡迎各位一起來討論技術呀!文章中有不足的地方小譚也歡迎各位指正探討,不勝感激!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM