又到了每年的面試季,一些換工作的朋友最近也正在加緊複習中,在這裏呢作者整理了十道前端面試中的精選問題和答案,希望對想要換工作的朋友有所幫助,同時如果在閱讀的過程中發現文章的問題,也請在評論區告知我。
React和Vue的區別?
- 數據流: React是單項數據流(props從父組件到子組件單向,數據到視圖單向),Vue則是雙向綁定(props在父子組件可以雙向綁定,數據和視圖雙向綁定)
-
數據監聽:React是在setState後比較數據引用的方式監聽,Vue通過Es5的數據劫持方法
defineProperty
監聽數據變化。 -
模板渲染:React是在js中通過原生的語法如if, map等方法動態渲染模板,Vue是通過
指令
來動態渲染模板。
爲什麼Vue數據頻繁變化但dom只會更新一次?
考慮以下場景:
export default {
data () {
return {
number: 0
};
},
mounted () {
var i = 0;
for(var i = 0; i < 1000; i++) {
this.number++;
}
}
}
當我們在一個同步任務中執行1000次number++操作,按照set -> 訂閱器(dep) -> 訂閱者(watcher) -> update的過程,dom應該會頻繁更新,按理說這會很消耗性能,但實際上這個過程中dom只會更新一次。
這是因爲vue的dom更新是一個異步操作,在數據更新後會首先被set
鉤子監聽到,但是不會馬上執行dom更新,而是在下一輪循環
中執行更新。
具體實現是vue中實現了一個queue
隊列用於存放本次事件循環
中的所有watcher
更新,並且同一個watcher
的更新只會被推入隊列一次,並在本輪事件循環的微任務
執行結束後執行此更新(UI Render
階段),這就是dom只會更新一次的原因。
js中的異步任務會進入任務隊列
在下一輪的事件循環中執行,更多事件循環的內容請參考Javascript運行機制之Event Loop。
如何理解React Fiber?
在React 16前,組件的更新是遞歸組件樹的方式實現的,這個遞歸更新的過程是同步的,如果組件樹過於龐大,實際更新過程會造成一些性能上的問題。
在React 16中發佈了React Fiber,React Fiber是一種能將遞歸更新組件樹的任務分片
(time-slicing
)執行的算法。它將組件樹的更新拆分爲多個子任務,在子任務的執行過程中,允許暫存當前進度然後執行其他優先級較高的任務
,如在更新過程中用戶產生了交互,那麼會優先去處理用戶交互,然後在迴歸更新任務繼續執行。更多相關React Fiber
你要如何設計一個報警系統?
從以下角度出發設計
1、項目錯誤信息採集
- 組件生命週期運行錯誤
- 事件中的錯誤
2、代碼埋點
- 在window.onerror、componentDidCatch、try catch等收集錯誤
- 在具體事件中埋點收集
3、上報時機
- 實時上傳埋點數據,適用於對錯誤收集的實時性有一定要求項目
- 定時上傳埋點數據。
考慮到服務器壓力因素,合理使用以上兩種上報方案,(答到服務器壓力點)
4、上報數據
- 錯誤詳細信息
- 用戶的環境數據採集(方便測試還原)
5、錯誤信息存儲
- 以時間命名的.log文件收集錯誤的詳細信息
- 在數據庫中存儲錯誤的統計信息用於錯誤的可視化展示
服務器上的日誌存儲方案有興趣可以自己瞭解,要是再講一講GFS
和HDFS
等分佈式文件系統應該有加分。
6、錯誤的統計和報警
- 以圖表的方式分類並按時間展示錯誤信息。
- 設定一個峯值爲報警值並郵件通知管理員
關於峯值的設定需要考慮到應用使用的高峯段和低谷段,並且此峯值需要在各個場景下不斷調優。
require和import的區別?
- require是運行時調用,import是編譯時調用(靜態加載)
- require是CommonJs規範,import是Es6的標準
- require的一個模塊就是一個文件,一個文件只能一個輸出。import的一個文件可以有多個輸出,並且在導入時可以選擇部分導入。
import在編譯時加載的特點使得其效率更高,也讓靜態分析和優化成爲了可能。webpack的tree shaking優化的基礎就是import的靜態導入。
require加載過程?
在node中使用require
和exports
時我們發現不管是在模塊中還是在全局對象上,都不存在這兩個方法,那麼這兩個方法是從何而來呢?
其實require方法本身是定義在Module中的,node在編譯階段將js文件包裝在函數將其包裝成模塊:
(function (exports, require, module, __filename, __dirname) {
file_content...
})
使用require加載模塊的過程
- 根據require的參數計算絕對路徑
path
- 根據
path
查找是否有緩存var cache = Module._cache[path]
,如果有緩存直接return
- 判斷是否是內置模塊如
http
,如果是直接return
- 生成模塊實例,並緩存
var module = new Module(path, parent);
Module._cache[path] = module;
- 加載模塊
module.load(path);
你知道哪些前端安全問題,如何避免?
1、XSS攻擊
- 反射型XSS
攻擊步驟
- 攻擊者構造出帶有惡意代碼的URL,誘導用戶點擊
- 服務端取出惡意代碼並拼接在
html
中返回給瀏覽器 - 瀏覽器執行惡意代碼,攻擊者獲取用戶信息或冒充用戶行爲進行攻擊
eg:
//惡意URL: http://xxx.com?key=<script>document.cookie</script>
//服務端拼接
<div>@{{params.key}}</div>
//最終瀏覽器執行了此惡意代碼獲取到用戶cookie
<div>
<script>document.cookie</script>
</div>
- 存儲型XSS
攻擊步驟
- 攻擊者在商品評論頁提交了惡意代碼到目標數據庫
- 用於訪問該商品,服務端將商品評論從數據庫取出並拼接在
HTML
中返回給瀏覽器 - 瀏覽器執行惡意代碼,攻擊者獲取用戶信息或冒充用戶行爲進行攻擊
存儲型XSS會將惡意代碼保存在數據庫中,會影響到所有使用的用戶,相比於反射型XSS造成後果更嚴重。
- DOM型XSS
DOM型XSS針對的主要是前端JS代碼的漏洞
攻擊步驟
- 攻擊者提供帶有惡意代碼的URL,或者已經存在於數據庫的惡意代碼
- 瀏覽器從URL中獲取惡意代碼,或者從後端接口中獲取到惡意代碼
- 前端
Javascript
直接執行了這些惡意代碼,造成DOM型XSS攻擊
eg:
//惡意URL: http://xxx.com?key=document.cookie
//前端取出key字段並執行
evel(location.key)
防範存儲和反射型XSS
- 前端渲染HTML,數據從接口中獲取
- 在服務端拼接HTML時轉義
防範DOM型XSS
- 小心innerHTML,outerHTML、eval等方法
- 小心setTimeout等能將字符串直接執行的方法
2、CSRF攻擊
CSRF攻擊實際上是利用了瀏覽器在向A域名發起請求前,會cookie
列表中查詢是否存在A域名的cookie。若是存在,則會將cookie添加至請求頭中的機制。
這個機制不在乎請求具體是從哪個域名發出,它只是關心目標路由。
攻擊步驟
- 用戶訪問正規網站
WebA
,瀏覽器保存下WebA
爲此用戶設置的cookie
- 攻擊者誘導用戶點擊不安全的網站
WebB
,此時從惡意網站WebB
向WebA
發送的請求中已經帶上了用戶的cookie
防範CSRF攻擊
- 如果是
Ajax
跨域請求,在Access-Control-Allow-Origin中設置安全的域名,如WebA
的域名。 - 如果是
form
表單請求,後端需要驗證http的Referer字段,確保來源是安全的。 - 推薦使用token驗證
3、自動化腳本攻擊
羊毛黨
通常使用腳本攻擊我們的線上活動,獲得非法利潤,他們通常使用刷API
接口,自動刷單
等方式獲取利潤。
通常來說,我們需要人機識別來防範腳本攻擊,在web前端
和服務端
之間,添加一層風控系統,用於鑑別終端是否是機器。
但前端依然可以爲羊毛黨
增加一些收入難度,想要薅羊毛
先得過前端這一關(紙老虎
)。
- token校驗,前端通過加密算法生成
token
,由風控系統校驗token
,攻擊者必須破解js生成token
的算法才能進行下一步。 - 代碼壓縮和混淆,這裏根據實際情設置混淆級別,太高級別的混淆可能會影響JS本身的執行效率。高級混淆後的代碼能防止攻擊者
斷點調試
- 收集用戶行爲,記錄用戶進入頁面中行爲,加密後交給風控系統(風控系統通過大數據分析地理位置、ip地址、行爲數據等進行人機識別分析)
tips:在前端的加密過程中,我們可以使用一些DOM、BOM,API,因爲攻擊者通過API
攻擊無法直接模擬出真實瀏覽器的環境,就算模擬也需要費一番功夫,加大攻擊者破解算法難度。
HTTPS如何實現安全加密傳輸?
- 客戶端發起請求,鏈接到服務器443端口
- 服務端的證書(自己製作或向三方機構申請),自己製作的證書需要客戶端驗證通過(用戶點一下)。證書中包含了兩個密鑰,一個公鑰一個私鑰。
- 服務端將公鑰返回到客戶端,公鑰中包含了證書頒發機構,證書過期時間等信息。
- 客戶端收到公鑰後,通過SSl/TSL層首先對公鑰信息進行驗證,如頒發機構過期時間等,如果發現異常,則會彈出一個警告框,提示證書存在問題。否則就生成一個隨機值,然後使用公鑰對此隨機值進行加密,此加密信息只能通過服務端的私鑰才能解密獲取生成的隨機值。
- 服務端獲取到加密信息後使用私鑰解密獲得隨機值,以後服務端和客戶端的通訊都會使用此隨機值進行加密,而這個時候,只有服務端和客戶端才知道這個隨機值(私鑰),服務端將要返回給客戶端的數據通過隨機值加密後返回。
- 客戶端用之前生成的隨機值解密服務段傳過來的信息,於是獲取瞭解密後的內容,整個過程第三方即使監聽到了數據,也束手無策。
HTTP/2如果實現首部壓縮?
HTTP/2通過維護靜態字典和動態字典的方式來壓縮首部
- 靜態字典中包含了常見的頭部名稱或者頭部名稱和值的組合,如method:GET
- 動態字典中包含了每個請求特有的鍵值對,如自定義的頭信息,針對每個TCP connection,都需要維護一份動態字典。
- 對於靜態字典中匹配的頭部名稱或頭部名稱和值的組合,可以使用一個字符表示,如建立連接時:
method:GET 可以使用 1表示 (完全匹配)
cookeie: xxx 可以使用 2:xxx表示 (頭部匹配) - 同時將cookeie: xxx加入動態字典中,這樣後續的整個cookie鍵值對都可以使用一個字符表示:
cookeie: xxx 可以使用 3表示 (加入到動態字典)
- 對於靜態字典和動態字典中都不存在的內容,使用了哈夫曼(霍夫曼)編碼來壓縮體積。
更多相關內容請查看詳情HTTP/2新特性
如何優化遞歸?
在Js代碼執行時,會產生一個調用棧,執行某個函數時會將其壓入棧,當它 return 後就會出棧。
而從某個函數調用另外一個函數時,就會爲被調用的函數建立一個新的棧幀,並且進入這個棧幀,這個棧幀稱爲當前棧,而調用函數的棧幀稱爲調用棧。
function A(){
return 1;
}
function B(){
A();
}
function C(){
B();
}
C();
Js執行棧中除了當前執行函數的棧幀,還保存着調用其函數的棧幀,在A釋放前,執行棧中保存了A、B、C的棧幀,過長的調用棧幀在Js中會導致一個棧溢出
的錯誤。
棧溢出
的錯誤常常出現在遞歸中。
當遞歸層次較深影響到代碼運行效率,甚至出錯後我們應該如何優化呢?
function fibonacci (n) {
if ( n <= 1 ) {
return 1
};
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(100) //卡死
1、尾遞歸優化
仔細觀察上述調用過程C -> B -> A,行程此調用棧的主要原因是在A執行完成後會將執行權返回B,此時B才能釋放,B釋放完成後繼續講執行權返回C,最後C釋放。
尾調用
尾調用(Tail Call)是函數式編程的一個重要概念,是指某個函數的最後一步是調用另一個函數。
尾調用由於是函數的最後一步操作,所以不需要保留外層函數的調用幀,所以在C調用B後就會釋放。
尾遞歸
函數調用自身,稱爲遞歸。如果尾調用自身,就稱爲尾遞歸。
將fibonacci函數使用尾遞歸優化
// 尾遞歸的優化往往是通過修改函數參數完成的
function fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return fibonacci2 (n - 1, ac2, ac1 + ac2);
}
面試官:尾調用是ES6的新功能吧,而且只有嚴格模式才能生效,因爲在非嚴格模式下,可以通過function.caller
追蹤到調用棧,還有其他方法嗎?
2、循環代替遞歸
使用蹦牀函數將遞歸轉爲循環執行
function trampoline(fn) {
while (fn && fn instanceof Function) {
fn = fn();
}
return fn;
}
function fibonacci3 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return fibonacci3.bind(null, n - 1, ac2, ac1 + ac2);
}
trampoline(fibonacci3(100)) //573147844013817200000
面試官:嗯,這樣確實可以避免棧溢出的錯誤問題,那你能嘗試下不使用遞歸思想實現求斐波那契數列的和呢?
3、使用動態規劃思想實現
function dp(n) {
if(n <= 1){
return 1
}
var a = 1;
var b = 2;
var temp = 0;
for(let i = 2; i < n; i++){
temp = a + b;
a = b;
b = temp;
}
return temp
}
最終我們對遞歸的優化就是放棄了使用遞歸😃