2022 Web 前端面試題及答案之JavaScript 篇
給大家分享一篇面試相關文章,希望大家在 2022 年,摸魚時間越來越多,薪資越漲越快!
1、事件循環機制
阿里面試題1:
- <script type="text/javascript">
- var p =new Promise(resolve=>{
- console.log(4)
- resolve(5)
- })
- function f1(){
- console.log(1)
- }
- function f2(){
- setTimeout(()=>{
- console.log(2)
- },0)
- f1()
- console.log(3)
- p.then(res=>{
- console.log(res)
- })
- }
- f2()
- </script>
- // 運行結果 4 1 3 5 2
- // 如果已經了解事件運行機制,就可以跳過該問題了
事件循環機制,event-loop 。包含三部分:調用棧、消息隊列、微任務隊列。
事件循環開始的時候,會從全局一行一行的執行代碼,遇到函數調用的時候,就會壓入調用棧中,當函數執行完成之后,彈出調用棧。
- // 如:代碼會一行一行執行,函數全部調用完成之后清空調用棧
- function f1(){
- console.log(1)
- }
- function f2(){
- f1()
- console.log(2)
- }
- f2()
- // 執行結果 1 2
如果遇到 fetch、setInterval、setTimeout 異步操作時,函數調用壓入調用棧時,異步執行內容會被加入消息隊列中,消息隊列中的內容會等到調用棧清空之后才會執行。
- // 如:
- function f1(){
- console.log(1)
- }
- function f2(){
- setTimeout(()=>{
- console.log(2)
- },0)
- f1()
- console.log(3)
- }
- f2()
- // 執行結果 :1 3 2
遇到 promise、async、await 異步操作時,執行內容會被加入微任務隊列中,會在調用棧清空之后立即執行。
調用棧加入的微任務隊列會立即執行。
- 如
- let p =new Promise(resolve=>{
- console.log('立即執行')
- resolve(1) //在 then 調用中執行
- })
微任務隊列中內容優先執行,所以比消息隊列中的內容執行得早。
了解這些知識后,再試一下最前面的那道面試題,應該就沒什么問題了。
2、你對作用域的認識有多少?
阿里面試題2:
- <script type="text/javascript">
- function fn(a,c){
- console.log(a)
- var a = 12
- console.log(a)
- console.log(c)
- function a(){ }
- if(false){
- var d = 34
- }
- console.log(d)
- console.log(b)
- var b = function(){}
- console.log(b)
- function c(){}
- console.log(c)
- }
- fn(1,2)
- </script>
- // 運行結果:
- /*
- function a(){}
- 12
- function c(){}
- undefined
- undefined
- function (){}
- function c(){}
- */
作用域通俗地講,就是指一個變量的作用范圍。下面分別介紹下全局作用域和函數作用域的概念。
全局作用域
- 頁面打開時被創建,頁面關閉時被銷毀。
- 編寫在 script 標簽下的變量和函數,作用域為全局,頁面的任意位置都可以訪問
- 有全局對象 window ,代表瀏覽器窗口,全局作用下的變量和函數作為 window 的屬性和方法
函數作用域(局部)
- 函數是被調用時創建的,執行完畢之后銷毀。
- 函數每調用一次,變量和函數就會重新創建一次,它們之間是相互獨立的
- 在函數作用域內可以訪問到全局變量或函數,但是在函數外無法訪問函數作用域內的變量
- 函數作用域內訪問變量,會在自身作用域內尋找,若沒有則會向上一級作用域內查找,一直到全局作用域。
講這些概念看完,發現還不會做上邊的面試題,接下來就學習學習作用域的預編譯,看看函數執行的時候都干了些啥?
函數在被調用的時候會先進行預編譯:
全局作用域預編譯:
- 創建上下文 GO 對象。
- 找變量聲明,將變量名作為 GO 對象的屬性名,值為 undefined
- 找函數式聲明,將值賦予函數體
函數作用域預編譯:
- 創建上下文 AO 對象
- 將形參和實參作為 AO 對象的屬性,賦值為 undefined
- 實參和形參相統一
- 在函數體內找函數聲明,將值賦予函數體。
了解預編譯過程之后,我們將上面的面試題進行解析,分析下運行結果是怎么來的?
fn 函數調用的時候,先進行預編譯,
第一階段:生成一個 AO 對象
第二階段:找到形參和實參,作為 AO 對象的屬性名,值為 udefined 。
- AO{
- a : undefined,
- b : undefined,
- c : undefined,
- d : undefined
- }
第三階段:實參和形參相統一,之后,AO對象改變為:
- AO{
- a : 1,
- b : undefined,
- c : 2,
- d : undefined
- }
第四階段:找到函數聲明,將值賦給變量,AO改變為:
- AO{
- a : function a(){ } ,
- b : undefined,
- c : function c(){ },
- d : undefined
- }
這下結合函數的預編譯過程以及函數作用域概念,再嘗試一下面試題,簡單了嗎?
3、為什么會有閉包?它解決了什么問題?
實例3:
- var liArr = document.getElementsByTagName('li')
- for(var i=0;i<liArr.length;i++){
- liArr[i].onclick = function(){
- console.log(liArr[i])
- }
- }
這是一個非常常見的實際應用,我們是想要點擊元素然后操作對應的元素,但是點擊之后發現打印出來的是 undefined 。我們應該能想到 i 變成了 liArr.length ,所以找不到對應元素,這個問題該如何解決呢?
說閉包時,必須介紹作用域。
上面介紹全局作用域和函數作用域,js內部變量的訪問是由內向外的,內部可以訪問到外部的變量,但是外部無法訪問函數內的變量,如果我們在外部訪問函數內的變量就需要使用閉包。
閉包就是函數嵌套函數,通過函數內的函數訪問變量的規則,實現外部訪問函數內的變量。
閉包的特點:
- 函數嵌套函數。
- 函數內部可以引用函數外部的參數和變量。
- 參數和變量不會被垃圾回收機制回收。
那么上述實例該如何使用閉包解決該問題呢?
實例3:閉包解決問題
- var liArr = document.getElementsByTagName('li')
- for(var i=0;i<liArr.length;i++){
- (function(i){
- liArr[i].onclick = function(){
- console.log('點擊元素',liArr[i])
- }
- })(i)
- }
閉包優點:
- 保護變量安全,實現封裝,防止變量聲明沖突和全局污染。
- 在內存當中維持一個變量,可以做緩存。
- 匿名函數自執行函數可以減少內存消耗。
防抖和節流就是閉包的經典應用。
4、防抖和節流,你了解多少?
在實際應用中,常見的就是窗口的 resize、輸入框搜索內容、scroll 等操作,如果這些操作觸發頻率太高,就會加重瀏覽器的負擔,同時用戶體驗也較差。該如何優化該操作呢?
防抖函數是什么呢?
當持續觸發事件,一定時間內沒有再觸發事件,事件處理函數才會執行一次,如果在設定的時間到來之前又觸發了事件,就會重新計時。
實例4:我們想要制作一個輸入框搜索,計劃輸入完成后兩秒再執行,打印出輸入的值。
- function debounce(val){
- var timer
- clearTimeout(timer)
- timer = setTimeout(function(){
- console.log(val)
- },2000)
- }
- var input = document.getElementById('input')
- input.addEventListener('keyup',function(e){
- debounce(e.target.value)
- })
實際運行結果:我們發現輸入之后,延時兩秒之后打印出結果。

并非我們想要的結果,這是什么原因呢?
因為函數每次重新調用的時候 timer 會重新創建,調用完成之后就會被銷毀,所以每次重新調用函數的時候,clearTimeout 內的 timer 都是 undefined 。所以我們需要把 timer 始終保持在內存當中,所以就需要使用閉包。
使用閉包修改上述實例4:
- function debounce(delay){
- var timer
- return function(val){
- clearTimeout(timer)
- timer = setTimeout(function(){
- console.log(val)
- },delay)
- }
- }
- var debounceFun = debounce(2000)
- var input = document.getElementById('input')
- input.addEventListener('keyup',function(e){
- debounceFun(e.target.value)
- })
防抖函數常見的實際應用:使用 echart 的時候,瀏覽器 resize 時,需要重新繪制圖表大小,還有典型的輸入框搜索應用。
節流函數是什么?
當持續觸發事件的時候,保證一段時間內只調用一次事件處理函數,一段時間內,只允許做一件事情。
實例5:滾動條實現一段時間內執行一次處理,執行回調。
- var throttle = function(func, delay) {
- var timer = null;
- return function() {
- var context = this;
- var args = arguments;
- if (!timer) {
- timer = setTimeout(function() {
- func.apply(context, args);
- timer = null;
- }, delay);
- }
- }
- }
- function handle() {
- console.log('執行回調');
- }
- window.addEventListener('scroll', throttle(handle, 1000));
防抖和節流主要是用來限制觸發頻率較高的事件,再不影響效果的前提條件下,降低事件觸發頻率,減小瀏覽器或服務器的壓力,提升用戶體驗效果。
5、數組去重有幾種方法?
這是一個非常常見的面試題,你知道幾種方式呢?
- var arr = [1,2,3,4,5,1,2,3,4]
- function unique(arr){
- //添加去重的方法的內容
- }
- unique(arr)
方法1: Set 方法
- return Array.from(new Set(arr))
- // 或
- return [...new Set(arr)]
new Set 返回的數據不是數組,所以使用 Aray.from 方法將類數組轉為真正的數組,或把 ...new Set(arr) 放入數組中。
方法2:使用兩次循環
- for(var i=0,len=arr.length;i<len;i++){
- for(var j=i+1,len=arr.length;j<len;j++){
- if( arr[i]===arr[j] ){
- arr.splice(i,1)
- j--;
- len--
- }
- }
- }
- return arr
方法3:indexOf 實現
arr.indexOf(item) 返回 item 元素在 arr 數組中第一次出現所在位置的下標。
- let arr1 = []
- for(var i=0;i<arr.length;i++){
- if( arr1.indexOf(arr[i]) === -1 ){
- arr1.push(arr[i])
- }
- }
- return arr1
方法4:includes 實現
- let arr1 = []
- for(var i=0;i<arr.length;i++){
- if( !arr1.includes(arr[i]) ){
- arr1.push(arr[i])
- }
- }
- return arr1
方法5:filter 實現
array.indexOf(item,start) start 表示開始檢索的位置。
- return arr.filter(( item, index )=>{
- return arr.indexOf( item, 0 ) == index
- })