JS面試題總結(持續更新)
1.JS有哪些數據類型?
根據JavaScript中的變量類型傳遞方式,分為基本數據類型和引用數據類型兩大類七種。
基本數據類型包括Undefined
、Null
、Boolean
、Number
、String
、Symbol
(ES6新增)六種。
引用數據類型只有Object
一種,主要包括對象、數組和函數。
判斷數據類型采用typeof
操作符,有兩種語法:
typeof 123;//語法一 const FG = 123; typeof FG;//語法二 typeof(null) //返回 object; null == undefined //返回true,因為undefined派生自null; null === undefined //返回false。
2. 基本數據類型和引用數據類型有什么區別?
(1) 兩者作為函數的參數進行傳遞時:
基本數據類型傳入的是數據的副本,原數據的更改不會影響傳入后的數據。
引用數據類型傳入的是數據的引用地址,原數據的更改會影響傳入后的數據。
(2) 兩者在內存中的存儲位置:
基本數據類型存儲在棧中。
引用數據類型在棧中存儲了指針,該指針指向的數據實體存儲在堆中。
3. 判斷數據類型的方法有哪些?
(1) 利用typeof
可以判斷數據的類型;
(2) A instanceof B
可以用來判斷A是否為B的實例,但它不能檢測null
和undefined
;
(3) B.constructor == A
可以判斷A是否為B的原型,但constructor
檢測Object
與instanceof
不一樣,還可以處理基本數據類型的檢測。
不過函數的 constructor 是不穩定的,這個主要體現在把類的原型進行重寫,在重寫的過程中很有可能出現把之前的constructor給覆蓋了,這樣檢測出來的結果就是不準確的。
(4) Object.prototype.toString.call()
Object.prototype.toString.call()
是最準確最常用的方式。
4. 與深拷貝有何區別?如何實現?
淺拷貝只復制指向某個對象的指針,而不復制對象本身。淺拷貝的實現方式有:
(1) Object.assign()
:需注意的是目標對象只有一層的時候,是深拷貝;
(2) 擴展運算符;
深拷貝就是在拷貝數據的時候,將數據的所有引用結構都拷貝一份。深拷貝的實現方式有:
(1) 手寫遍歷遞歸賦值;
(2)結合使用JSON.parse()
和JSON.stringify()
方法。
5. let、const的區別是什么?
var
、let
、const
都是用于聲明變量或函數的關鍵字。其區別在于:
6. 什么是執行上下文和執行棧?
變量或函數的執行上下文,決定了它們的行為以及可以訪問哪些數據。每個上下文都有一個關聯的變量對象,而這個上下文中定義的所有變量和函數都存在于這個對象上(如DOM中全局上下文關聯的便是window
對象)。
每個函數調用都有自己的上下文。當代碼執行流進入函數時,函數的上下文被推到一個執行棧中。在函數執行完之后,執行棧會彈出該函數上下文,在其上的所有變量和函數都會被銷毀,并將控制權返還給之前的執行上下文。 JS的執行流就是通過這個執行棧進行控制的。
7. 什么是作用域和作用域鏈?
作用域可以理解為一個獨立的地盤,可以理解為標識符所能生效的范圍。作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。ES6中有全局作用域、函數作用域和塊級作用域三層概念。
當一個變量在當前塊級作用域中未被定義時,會向父級作用域(創建該函數的那個父級作用域)尋找。如果父級仍未找到,就會再一層一層向上尋找,直到找到全局作用域為止。這種一層一層的關系,就是作用域鏈 。
8. 作用域和執行上下文的區別是什么?
(1) 函數的執行上下文只在函數被調用時生成,而其作用域在創建時已經生成;
(2) 函數的作用域會包含若干個執行上下文(有可能是零個,當函數未被調用時)。
9. this指向的各種情況都有什么?
this的指向只有在調用時才能被確定,因為this
是執行上下文的一部分。
(1) 全局作用域中的函數:其內部this
指向window
:
var a = 1; function fn(){ console.log(this.a) } fn() //輸出1
(2) 對象內部的函數:其內部this
指向對象本身:
var a = 1; var obj = { a:2, fn:function(){ console.log(this.a) } } obj.fn() //輸出2
(3) 構造函數:其內部this
指向生成的實例:
function createP(name,age){ this.name = name //this.name指向P this.age = age //this.age指向P } var p = new createP("老李",46)
(4) 由apply
、call
、bind
改造的函數:其this
指向第一個參數:
function add(c,d){ return this.a + this.b + c + d } var o = {a:1,b:2) add.call(o,5,7) //輸出15
(5) 箭頭函數:箭頭函數沒有自己的this
,看其外層的是否有函數,如果有,外層函數的this
就是內部箭頭函數的this
,如果沒有,則this
是window
。
10.如何改變this指針的指向?
可以使用apply
、call
、bind
方法改變this
指向(并不會改變函數的作用域)。比較如下:
(1) 三者第一個參數都是this
要指向的對象,也就是想指定的上下文,上下文就是指調用函數的那個對象(沒有就指向全局window);
(2) apply
和bind
的第二個參數都是數組,call
接收多個參數并用逗號隔開;
(3) apply
和call
只對原函數做改動,bind
會返回新的函數(要生效還得再調用一次)。
11.什么是閉包?
12. 什么是原型、原型鏈?
13. 何為防抖和節流?如何實現?
防抖和節流都是防止短時間內高頻觸發事件的方案。
防抖的原理是:如果一定時間內多次執行了某事件,則只執行其中的最后一次。
節流的原理是:要執行的事件每隔一段時間會被冷卻,無法執行。
應用場景有:搜索框實時搜索,滾動改變相關的事件。
[email protected]: 要執行的函數 [email protected]: 設定的時限 //防抖函數 function debunce(fn, delay) { let flag = null; return function() { if (flag) clearTimeout(flag) //利用apply改變函數指向,使得封裝后的函數可以接收event本身 flag = setTimeout(() = >fn.apply(this, arguments), delay) } } //節流函數 function throttle(fn, delay) { let flag = true; return function() { if (!flag) return false; flag = false; setTimeout(() = >{ fn.apply(this, arguments) flag = true }, delay) } }
14. 如何理解同步和異步?
同步:按照代碼書寫順序一一執行處理指令的一種模式,上一段代碼執行完才能執行下一段代碼。
異步:可以理解為一種并行處理的方式,不必等待一個程序執行完,可以執行其它的任務。
JS之所以需要異步的原因在于JS是單線程運行的。常用的異步場景有:定時器、ajax請求、事件綁定。
15. JS是如何實現異步的?
JS引擎是單線程的,但又能實現異步的原因在于事件循環和任務隊列體系。
事件循環:
JS會創建一個類似于while(true)
的循環,每執行一次循環體的過程稱之為Tick
。每次Tick
的過程就是查看是否有待處理事件,如果有則取出相關事件及回調函數放入執行棧中由主線程執行。待處理的事件會存儲在一個任務隊列中,也就是每次Tick
會查看任務隊列中是否有需要執行的任務。
任務隊列:
異步操作會將相關回調添加到任務隊列中。而不同的異步操作添加到任務隊列的時機也不同,如onclick
,setTimeout
,ajax
處理的方式都不同,這些異步操作是由瀏覽器內核的webcore
來執行的,瀏覽器內核包含3種webAPI
,分別是DOM Binding
、network
、timer
模塊。
onclick
由DOM Binding
模塊來處理,當事件觸發的時候,回調函數會立即添加到任務隊列中。
setTimeout
由timer
模塊來進行延時處理,當時間到達的時候,才會將回調函數添加到任務隊列中。
ajax
由network
模塊來處理,在網絡請求完成返回之后,才將回調添加到任務隊列中。
主線程:
JS只有一個線程,稱之為主線程。而事件循環是主線程中執行棧里的代碼執行完畢之后,才開始執行的。所以,主線程中要執行的代碼時間過長,會阻塞事件循環的執行,也就會阻塞異步操作的執行。
只有當主線程中執行棧為空的時候(即同步代碼執行完后),才會進行事件循環來觀察要執行的事件回調,當事件循環檢測到任務隊列中有事件就取出相關回調放入執行棧中由主線程執行。
16. 什么是AJAX?如何實現?
ajax
是一種能夠實現局部網頁刷新的技術,可以使網頁異步刷新。
ajax
的實現主要包括四個步驟:
(1) 創建核心對象XMLhttpRequest
;
(2) 利用open
方法打開與服務器的連接;
(3) 利用send
方法發送請求;(”POST”請求時,還需額外設置請求頭)
(4) 監聽服務器響應,接收返回值。
//1-創建核心對象 //該對象有兼容問題,低版本瀏覽器應使用ActiveXObject const xthhp = new XMLHttpRequest(); //2-連接服務器 //open(method,url,async) xhttp.open("POST","http://localhost:3000",true) //設置請求頭 xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); //3-發送請求 //send方法發送請求參數,如為GET方法,則在open中url后拼接 xhttp.send({_id:123}) //4-接收服務器響應 //onreadystatechange事件,會在xhttp的狀態發生變化時自動調用 xhttp.onreadystatechange =function(){ //狀態碼共5種:0-未open 1-已open 2-已send 3-讀取響應 4-響應讀取結束 if(xhttp.readyState == 4 && xhttp.status == 200){ alert("ajax請求已完成") } }
17. 實現異步的方式有哪些?
(1) 回調函數模式:將需要異步執行的函數作為回調函數執行,其缺點在于處理復雜邏輯異步邏輯時,會造成回調地獄(回調嵌套層數太多,代碼結構混亂);
(2) 事件監聽模式:采用事件驅動的思想,當某一事件發生時觸發執行異步函數,其缺點在于整個代碼全部得變為事件驅動模式,難以分辨主流程;
(3) 發布訂閱模式:當異步任務執行完成時發布消息給信號中心,其他任務通過在信號中心中訂閱消息來確定自己是否開始執行;
(4) Promise(ES6):Promise
對象共有三種狀態pending
(初始化狀態)、fulfilled
(成功狀態)、rejected
(失敗狀態)。
(5)async/await(ES7):基于Promise
實現的異步函數;
(6) 利用生成器實現。
18. 如何理解Promise對象?
Promise
對象有如下兩個特點:
(1) 對象的狀態不受外界影響。Promise
對象共有三種狀態pending
、fulfilled
、rejected
。狀態值只會被異步結果決定,其他任何操作無法改變。
(2) 狀態一旦成型,就不會再變,且任何時候都可得到這個結果。狀態值會由pending
變為fulfilled
或rejected
,這時即為resolved
。
Promise的缺點有如下三個缺點:
(1) Promise
一旦執行便無法被取消;
(2) 不可設置回調函數,其內部發生的錯誤無法捕獲;
(3) 當處于pending
狀態時,無法得知其具體發展到了哪個階段。
Pomise
中常用的方法有:
(1) Promise.prototype.then()
:Promise
實例的狀態發生改變時,會調用then
內部的回調函數。then
方法接受兩個參數(第一個為resolved
狀態時時執行的回調,第一個為rejected
狀態時時執行的回調)
(2) Promise.prototype.catch()
:.then(null, rejection)
或.then(undefined, rejection)
的別名,用于指定發生錯誤時的回調函數。
19.如何理解宏任務,微任務?
宏任務有:script
(整體代碼)、setTimeout
、setInterval
、I/O
、頁面渲染;
微任務有:Promise.then
、Object.observe
、MutationObserver
。
執行順序大致如下:
主線程任務——>宏任務——>微任務——>微任務里的宏任務——>…….——>直到任務全部完成
20. 什么是跨域?如何解決跨域問題?
跨域問題實際是由同源策略衍生出的一個問題,當傳輸協議、域名、端口任一部分不一致時,便會產生跨域問題,從而拒絕請求,但<img src=XXX> <link href=XXX><script src=XXX>
;天然允許跨域加載資源。
解決方案有:
(1) JSONP
原理:利用<script>;標簽沒有跨域限制的漏洞,使得網頁可以得到從其他來源動態產生的JSON數據(前提是服務器支持)。
優點:實現簡單,兼容性好。
缺點:僅支持get方法,容易受到XSS攻擊。
(2) CORS
原理:服務器端設置Access-Control-Allow-Origin
以開啟CORS。該屬性表示哪些域名可以訪問資源,如設置通配符則表示所有網站均可訪問。
實現實例(express):
//app.js中設置 var app = express(); //CORS跨域------------------------------------------------------------------------------------- // CORS:設置允許跨域中間件 var allowCrossDomain = function (req, res, next) { // 設置允許跨域訪問的 URL(* 表示允許任意 URL 訪問) res.header("Access-Control-Allow-Origin", "*"); // 設置允許跨域訪問的請求頭 res.header("Access-Control-Allow-Headers", "X-Requested-With,Origin,Content-Type,Accept,Authorization"); // 設置允許跨域訪問的請求類型 res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); // 設置允許服務器接收 cookie res.header('Access-Control-Allow-Credentials', 'true'); next(); }; app.use(allowCrossDomain); //------------------------------------------------------------------------------------
(3) Node中間件代理
原理:同源策略僅是瀏覽器需要遵循的策略,故搭建中間件服務器轉發請求與響應,達到跨域目的。
/* server1.js 代理服務器(http://localhost:3000)*/ const http = require('http') // 第一步:接受客戶端請求 const server = http.createServer((request, response) = >{ // 代理服務器,直接和瀏覽器直接交互,需要設置CORS 的首部字段 response.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': 'Content-Type' }) // 第二步:將請求轉發給服務器 const proxyRequest = http.request({ host: '127.0.0.1', port: 4000, url: '/', method: request.method, headers: request.headers }, serverResponse = >{ // 第三步:收到服務器的響應 var body = ''serverResponse.on('data', chunk = >{ body += chunk }) serverResponse.on('end', () = >{ console.log('The data is ' + body) // 第四步:將響應結果轉發給瀏覽器 response.end(body) }) }).end() }) server.listen(3000, () = >{ console.log('中間件服務器地址: http://localhost:3000') }) // server2.js(http://localhost:4000) const http = require("http"); const data = { title: "fontend", password: "123456" }; const server = http.createServer((request, response) = >{ if (request.url === "/") { response.end(JSON.stringify(data)); } }); server.listen(4000, () = >{ console.log("The server is running at http://localhost:4000"); });
(4) nginx反向代理
原理:類似Node中間件服務器,通過nginx代理服務器實現。
實現方法:下載安裝nginx,修改配置。
21. 實現繼承的方法有哪些?
(1) class+extends繼承(ES6)
//類模板 class Animal { constructor(name){ this.name = name } } //繼承類 class Cat extends Animal{//重點。extends方法,內部用constructor+super constructor(name) { super(name); //super作為函數調用時,代表父類的構造函數 }//constructor可省略 eat(){ console.log("eating") } }
(2) 原型繼承
//類模板 function Animal(name) { this.name = name; } //添加原型方法 Animal.prototype.eat = function(){ console.log("eating") } function Cat(furColor){ this.color = color ; }; //繼承類 Cat.prototype = new Animal()//重點:子實例的原型等于父類的實例
(3) 借用構造函數繼承
function Animal(name) { this.name = name } function Cat() { Animal.call(this, "CatName") //重點,調用父類的call方法 }
(4) 寄生組合式繼承(重點)
組合式繼承的不足
組合繼承是JavaScript最常用的繼承模式,但也有它的不足:
- 無論什么情況下,都會調用兩次超類構造函數
- ?子類會包含超類對象全部的實例屬性,但又不得不在調用子類構造函數時重寫這些屬性
先看一看組合繼承的例子:
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name, age) { SuperType.call(this, name); // 第二次調用 SuperType() this.age = age; } SubType.prototype = new SuperType(); // 第一次調用 SuperType() SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { alert(this.age); };
如代碼所示,在第一次調用 SuperType
構造函數SubType.prototype
會得到兩個屬性: name
和 colors
;它們都是 SuperType
的實例屬性,只不過現在位于 SubType
的原型中。
當調用 SubType
構造函數時,又會調用一次 SuperType
構造函數,這一次又在新對象上創建了實例屬性 name
和 colors
。于是,這兩個屬性就屏蔽了原型中的兩個同名屬性
解決方案:
有兩組 name
和 colors
屬性:一組在實例上,一組在 SubType
原型中,這是不合理的。解決這個問題方法是:寄生組合式繼承。
寄生組合式繼承基本模式:
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //創建對象 prototype.constructor = subType; //增強對象 subType.prototype = prototype; //指定對象 }
這個函數接收兩個參數:子類型構造函數和超類型構造函數, 執行以下步驟:
- 第一步是創建超類型原型的一個副本。
- 為創建的副本添加 constructor 屬性,從而彌補因重寫原型而失去的默認的 constructor 屬性。
- 將新創建的對象(即副本)賦值給子類型的原型。
現在我們就可以用調用 inheritPrototype() 函數的語句,去替換前面例子中為子類型原型賦值的語句了,修改后的代碼如下:
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); };
這個例子的高效率體現在它只調用了一次 SuperType
構造函數,并且因此避免了在 SubType.prototype
上面創建不必要的、多余的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof
和 isPrototypeOf()
。普遍認為寄生組合式繼承是引用類型最理想的繼承范式。
22. DOM事件模型和事件流?
DOM事件模型包括事件捕獲(自上而下觸發)與事件冒泡(自下而上觸發,ie用的就是冒泡)機制?;谑录芭輽C制可以完成事件代理。
事件捕獲
事件冒泡
DOM事件流包括三個階段事件捕獲階段、處于目標階段、事件冒泡階段。
23. EventLoop事件循環是什么?
js是一門單線程的需要,它的異步操作都是通過事件循環來完成的。整個事件循環大體由執行棧、消息隊列和微任務隊列三個部分組成。
同步代碼會直接在執行棧中調用執行。
定時器中的回調會在執行棧被清空且定時達成時推入執行棧中執行。
promise
、async
異步函數的回調會被推入到微任務隊列中,當執行棧被清空且異步操作完成時立即執行。
24. require/import之間的區別?
require
是CommonJS語法,import
是ES6語法;require
只在后端服務器支持,import
在高版本瀏覽器及Node中都可以支持;require
引入的是原始導出值的復制,import
則是導出值的引用;require
時運行時動態加載,import
是靜態編譯;require
調用時默認不是嚴格模式,import
則默認調用嚴格模式.
1. 本站所有文章教程及資源素材均來源于網絡與用戶分享或為本站原創,僅限用于學習和研究。
2. 如果內容損害你的權益請聯系客服QQ:1642748312給予處理。
碼云筆記 » JS面試題總結(持續更新)