「2022」打算跳槽漲薪,必問面試題及答案 -- JavaScript 篇


Hi,我是前端人,今日與君共勉!
 
「2022」高頻前端面試題匯總之 JavaScript 篇

1、深淺拷貝的區別有哪些?

要說 js 的深淺拷貝,就不得不提 js 的兩大數據類型:基本數據類型和引用類型。基本數據類型的變量名和值都存儲在棧中,對於引用類型的變量名存儲在棧中,而值存儲在堆中。由於存儲方式不同,所以導致了他們復制的時候方式不同。

淺拷貝是創建一個新對象,這個對象有着原始對象屬性值的一份精准拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果是引用類型,拷貝的就是內存地址,所以如果其中一個對象改變了這個地址,就會影響到另外一個對象。

深拷貝是將一個對象從內存中完整的拷貝一份出來,從內存堆中存放一個新的對象。這是兩個對象,所以修改其中一個,另外一個不會受影響。

深淺拷貝主要針對的是引用類型,簡單數據類型不受影響。

相關筆試題

var person = {
 name:"前端人",
 hobby:['學習','敲代碼','潛水']
}
function copy(source){
 var newObj = new Object()
 for(var i in source){
  if(source.hasOwnProperty(i)){
   newObj[i] = source[i]
   }
  }
 return newObj
}
var p1 = copy(person);
p1.name = "Web Person"
console.log(person.name)
console.log(p1.name)
p1.hobby = ["內卷"]
console.info(person.hobby)
console.info(p1.hobby)
/*運行結果:
前端人
 Web Person
["學習", "敲代碼", "潛水"]
["內卷"]
*/

 

2、js 數據類型有哪些?

js 數據類型一共有 8 種,分為兩大類:基本類型和引用類型。

它們的數據類型分別為:

基本類型:string、number、boolean、null、undefined、symbol、bigint

引用類型:object

相關面試題

// 注意:其他類型與數值進行相加時,其他類型的轉為 number 類型
console.log( true+1 ) // 2
console.log( undefined +1 ) // NaN

console.log( null ) //object
console.log( undefined ) // undefined

 

3、延遲加載 js 的方式有哪些?有什么區別呢?

共有 6 種方式,分別為:

  • async
  • defer
  • js 最后加載
  • 利用 setTimeout
  • 動態創建 DOM 的方式
  • 使用 jQuery 的 getScript 方法

它們的區別介紹:

1、async:為 <script>標簽定義了 async 屬性。async 和 html 解析是同步的,不是順次執行 js 腳本,誰先加載完成先執行誰。

<script  async type="text/javascript" src="demo1.js" ></script>
<script  async type="text/javascript" src="demo2.js" ></script>

2、defer 會等到 html 解析完成之后再執行 js 代碼,如果有多個腳本時,會按照順序依次執行腳本。

<script  defer type="text/javascript" src="demo1.js" ></script>

3、js 最后加載

把 js 外部引入的文件放置在頁面的底部,讓 js 最后加載,從而加快頁面加載速度。

4、利用 setTimeout

5、動態創建 DOM 的方式

var element = document.createElement("script");  
element.src = "box.js";  
document.body.appendChild(element);

這種方式通過操作動態加載 js 文件,不觸發的時候不加載,減少頁面文件大小,加快加載速度。

6、使用 jQuery 的 getScript 方法

$.getScript( "box.js",function(){//回調函數,成功獲取文件后執行的函數  
      console.log("腳本加載完成")  
});

相關面試題:

<!doctype html>
<html>
 <head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="text/javascript" src="box.js"></script>
 </head>
 <body>
  <div id="box"></div>
 </body>
</html>

//box.js 代碼如下
console.log( document.getElementById('box') )  // null

box.js 想正常獲取元素 box ,並進行一系列操作應該如何延遲加載 js 文件呢?

4、你對作用域的認識有多少?

作用域通俗地講,就是指一個變量的作用范圍。分為全局作用域和函數作用域。

全局作用域

  • 頁面打開時被創建,頁面關閉時被銷毀。
  • 編寫在 script 標簽下的變量和函數,作用域為全局,頁面的任意位置都可以訪問
  • 有全局對象 window ,代表瀏覽器窗口,全局作用下的變量和函數作為 window 的屬性和方法

函數作用域(局部)

  • 函數是被調用時創建的,執行完畢之后銷毀。
  • 函數每調用一次,變量和函數就會重新創建一次,它們之間是相互獨立的
  • 在函數作用域內可以訪問到全局變量或函數,但是在函數外無法訪問函數作用域內的變量
  • 函數作用域內訪問變量,會在自身作用域內尋找,若沒有則會向上一級作用域內查找,一直到全局作用域。

函數在被調用的時候會先進行預編譯:

全局作用域預編譯:

  • 創建上下文 GO 對象。
  • 找變量聲明,將變量名作為 GO 對象的屬性名,值為 undefined
  • 找函數式聲明,將值賦予函數體

函數作用域預編譯:

  • 創建上下文 AO 對象
  • 將形參和實參作為 AO 對象的屬性,賦值為 undefined
  • 實參和形參相統一
  • 在函數體內找函數聲明,將值賦予函數體。

相關面試題:

<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(){}
*/

 

5、null 和 undefined 的區別。

null 和 undefined 兩個都表示無的值。

作者設計 js 的時候,借鑒的 java 語言先設計的 null 。null 使用的時候會被隱式轉化成 0,不容易發現錯誤。

console.log( number(null) ) //0

undefined 是為了填補 null 的坑。所以后來又新增了 undefined 。

console.log( number(undefined) ) //NaN

 

6、new 操作符具體做了什么?

  • 創建了一個空對象。
  • 將空對象的原型指向於構造函數的原型。
  • 將空對象作為構造函數的上下文。
  • 對構造函數有返回值的處理判斷。

實現 new 操作符的方法:

function create( fn,...args ){
 var obj={}
 Object.setPrototypeOf( obj,fn.prototype )
 var resault = fn.apply(obj,args)
 return (resault instanceof Object) ? result : obj
}

 

7、為什么會有閉包?它解決了什么問題?

7.1、什么是閉包?

閉包就是函數嵌套函數,通過函數內的函數訪問變量的規則,實現外部訪問函數內的變量。

7.2、閉包的特點:

  • 函數嵌套函數。
  • 函數內部可以引用函數外部的參數和變量。
  • 參數和變量不會被垃圾回收機制回收。

實例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) 
}

7.3、閉包優點:

  • 保護變量安全,實現封裝,防止變量聲明沖突和全局污染。
  • 在內存當中維持一個變量,可以做緩存。
  • 匿名函數自執行函數可以減少內存消耗。

防抖和節流就是閉包的經典應用。

7.4、閉包缺點:

  • 變量會駐留在內存中,造成內存損耗問題。解決辦法:把閉包函數設置為 null 。
  • 內存泄漏

8、防抖和節流,你了解多少?

8.1、什么是防抖函數?

當持續觸發事件,一定時間內沒有再觸發事件,事件處理函數才會執行一次,如果在設定的時間到來之前又觸發了事件,就會重新計時。

防抖函數常見的實際應用:使用 echart 的時候,瀏覽器 resize 時,需要重新繪制圖表大小,還有典型的輸入框搜索應用。

8.2、節流函數是什么?

當持續觸發事件的時候,保證一段時間內只調用一次事件處理函數,一段時間內,只允許做一件事情。

防抖和節流主要是用來限制觸發頻率較高的事件,再不影響效果的前提條件下,降低事件觸發頻率,減小瀏覽器或服務器的壓力,提升用戶體驗效果。

9、數組去重有幾種方法?

方法1: new set()

return Array.from(new Set(arr))
//
return [...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 實現

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

 

10、call、bind 和 apply 的區別

三者都是改變函數執行的上下文,即改變 this 指向。

它們之間的區別為:

  • call 和 apply 會立即執行,bind 返回的是一個函數,需調用后執行。
  • 第二參數是傳入要執行的方法中的參數,call 和 bind 是獨立傳遞參數,apply 是以數組傳遞參數的

使用場景:
1、需要改變某個函數的this指向時
2、當參數較少時可以使用call,參數較多可以使用apply以數組的方式傳遞
3、當需要重復調用時,可以使用bind新定義一個方法

11、js 判斷變量是不是數組,你能寫出幾種方法?

方法1:isArray

var arr = [1,2,3]
console.log(Array.isArray(arr))  

方法2:instanceof

var arr = [1,2,3]
console.log( arr instanceof Array )
console.log( arr instanceof Object )

該方法不夠嚴謹。

方法3:prototype

console.log( Object.prototype.toString.call(arr).indexOf('Array')>-1 )

方法4:isPrototypeOf

console.log( Array.prototype.isPrototypeOf( arr ) )

方法5:constructor

console.log(arr.constructor.toString().indexOf('Array')>-1 )

 

12、slice 是干嘛的? splice 是否會改變原數組?

slice 是用來截取字符串的,返回一個新數組,但不會影響原數組。

使用語法:

arr.slice( start , end )

截取 arr 數組,從 start 開始到 end 結束,第二個參數是可選參數,沒有時從 start 開始截取到結尾。

如果 start 參數是負數時,就會從 arr.lengtn + start 開始截取到結束。

var arr = ['a','b','c','d','e']
console.log( arr.slice(-3) ) // ["c", "d", "e"]
console.log(arr)  //["a", "b", "c", "d", "e"]

splice 是一個更強大的方法,可以添加、刪除、替換數組元素,返回的是被刪除元素,它的操作會改變原數組。

使用語法:

splice( start, n, new )

從 start 開始,刪除 n 個元素,然后把 new 添加到 start 元素之后。第三個參數為可選參數

  • n 為 0 且第三個參數不為空時,表示添加新元素到 start 之后。
  • n 不為 0 且第三個參數不為空時,表示把 start 之后的 n 個元素替換成 new 。
  • n 不為 0 且第三個參數為空時,表示刪除 start 后的 n 個元素。
var arr = ['a','b','c','d','e']
var ar = arr.splice( 1, 1 ,'f','g')
console.log('ar',ar)    // ["b"]
console.log('arr',arr) //  ["a", "f", "g", "c", "d", "e"]

 

13、== 和 === 有什么不同?

== 比較的是值,=== 除了比較值,還比較類型。

console.log( [1,2]=='1,2'  )       // true
console.log( [1,2] === '1,2'  )  //false

valueOf 方法返回 Math 對象的原始值,通常由 javascript 在后台自動調用,並不顯示的出現在代碼中。

console.log([1,2].valueOf()) //[1,2]
console.log('1,2'.valueOf()) //[1,2]
// 所以
console.log( [1,2]=='1,2'  )  // true

不管是字符串和數字比較,還是布爾值和數字比較,都會使用 valueOf 隱式轉換。

總結:== 需要使用 valueOf() 進行隱式轉換,所以性能差。 === 會避開一些不必要的麻煩。

14、this 的指向

大廠筆試題:

var name = 'window name'
var p1 = {
 name:'p1 name',
 showName:function(){
  console.info(this.name)
 }
}
var fn = p1.showName
fn()
p1.showName()
var p2 = {
 name:'p2 name',
 showName:function(fun){
  fun()
 }
}
p2.showName(p1.showName)
p2.showName = p1.showName
p2.showName()
/*
運行結果:
window name
 p1 name
 window name
 p2 name
*/

這是一道關於 this 指向的面試題,接下來我們就說說 this 是如何指向的?

this 對象是運行時基於函數的執行環境綁定的:

  • 在全局函數中,this 等於 window 。
  • 函數上下文調用,嚴格模式下 this 為 undefined ,非嚴格模式下,this 指向 window 。
  • 當函數被作為某個對象的方法被調用時,this 等於那個對象。如果使用 call apply 改變當前 this 時,將會指向為傳遞過來的那個 this 。
  • 匿名函數的執行環境具有全局性,因此 this 指向 window。
  • 構造函數內的 this 指向創建的實例對象。
  • dom 事件處理函數,this 指向觸發該事件的元素。
  • setTimeout 和 setInterval 中的 this 指向全局變量 window

15、js 中的繼承有哪些方式呢?

第 1 種:原型鏈繼承

function Parent(){
 this.name = "前端人"
}
Parent.prototype.showName = function(){
 console.log(this.name)
}
function Child(){}
 //原型鏈繼承   
Child.prototype = new Parent()
var p = new Child()
console.dir(p.name) //前端人

特點:

  • 實例的是子類的實例,也是父類的實例。
  • 父類新增原型方法和屬性,子類都能訪問到。
  • 簡單,方便實現

第 2 種:借用構造函數

function Animal (name) {
 this.name = name || 'Animal';
 this.sleep = function(){
  console.log(this.name + '正在睡覺!');
 }
}
Animal.prototype.eat = function(food) {
 console.log(this.name + '正在吃:' + food);
};
function Cat(name){
 Animal.call(this);
 this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特點:

  • 創建子類時,可以向父類傳遞參數。
  • 可以實現多繼承,call 多個父類對象。
  • 解決方法1中,子類實例共享父類引用屬性的問題。

還有組合式繼承、ES6 的繼承 和 寄生組合繼承等等。每種繼承方式都有各自的特點和缺點。

16、嚴格模式與非嚴格模式的區別,你了解多少?

JavaScript 語言是一門弱類型語言,存在許多類型錯誤,因此 ES6 引入了嚴格模式概念。

如果不加 ‘use strict’ 常規模式下就是屬於非嚴格模式。

嚴格模式

在 js 文件頂部添加 ‘use strict’ 就屬於嚴格模式,嚴格模式也可以指定在函數內部。

<script>
 'use strict'  
 //或者函數內部
 (function(){
  'use strict'
 })()
</script>

嚴格模式,是為 js 定義來了一種不同的解析與執行模型,在嚴格模式下,ECMAScipt 3 中一些不解和不確定的行為將得到處理,而且會對不安全的操作會拋出異常。‘use strict’ 會告訴瀏覽器引擎可以切換到嚴格模式執行。

嚴格模式與非嚴格模式區別

嚴格模式

非嚴格模式

變量必須聲明才能賦值

變量不進行聲明,可直接賦值

不能使用 delete 字符刪除變量或對象

可以使用 delete 刪除

函數參數變量名不允許重復

變量名重復,獲取最后最后那個值

普通函數內的 this 為 undefined

普通函數內的 this 為 window

不允許使用八進制

允許任意進制

eval 和 arguments 當做關鍵字,不能被賦值和用作變量名

可以使用 eval 、arguments 作為變量名

call、apply 傳入 null undefined 保持原樣不被轉為window

默認轉為 window 對象

限制對調用棧的檢測能力,訪問 arguments.callee 會拋出異常

arguments.callee 運行正常

17、隱式轉化相關面試題

console.log( '2'>10 ) //false
console.log( '2'>'10' ) //true
console.log( 'abc'>'b' ) //false
console.log( 'abc'>'aab' ) //true
console.log( undefined == null ) //true
console.log( NaN == NaN )//false
console.log( [] == 0 ) //true
console.log( ![] == 0 ) //true
console.log( [] == [] ) //false
console.log( {} == {} ) //false
console.log( {} == !{} ) //false

 

18、事件循環機制相關面試題。

阿里面試題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 調用中執行
})

微任務隊列中內容優先執行,所以比消息隊列中的內容執行得早。

了解這些知識后,再試一下最前面的那道面試題,應該就沒什么問題了。

20、前端領域內,你比較擅長什么?

這個問題就留給讀到最后,能夠堅持學習的人,問問我們自己有什么是我們擅長的?在哪塊領域是我們占據競爭優勢的?


免責聲明!

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



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