2019十道精選前端面試題整理彙總

又到了每年的面試季,一些換工作的朋友最近也正在加緊複習中,在這裏呢作者整理了十道前端面試中的精選問題和答案,希望對想要換工作的朋友有所幫助,同時如果在閱讀的過程中發現文章的問題,也請在評論區告知我。

原文鏈接

React和Vue的區別?

  1. 數據流: React是單項數據流(props從父組件到子組件單向,數據到視圖單向),Vue則是雙向綁定(props在父子組件可以雙向綁定,數據和視圖雙向綁定)
  2. 數據監聽:React是在setState後比較數據引用的方式監聽,Vue通過Es5的數據劫持方法defineProperty監聽數據變化。
  3. 模板渲染: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文件收集錯誤的詳細信息
  • 在數據庫中存儲錯誤的統計信息用於錯誤的可視化展示
服務器上的日誌存儲方案有興趣可以自己瞭解,要是再講一講GFSHDFS等分佈式文件系統應該有加分。

6、錯誤的統計和報警

  • 以圖表的方式分類並按時間展示錯誤信息。
  • 設定一個峯值爲報警值並郵件通知管理員
關於峯值的設定需要考慮到應用使用的高峯段和低谷段,並且此峯值需要在各個場景下不斷調優。

require和import的區別?

  • require是運行時調用,import是編譯時調用(靜態加載)
  • require是CommonJs規範,import是Es6的標準
  • require的一個模塊就是一個文件,一個文件只能一個輸出。import的一個文件可以有多個輸出,並且在導入時可以選擇部分導入。
import在編譯時加載的特點使得其效率更高,也讓靜態分析和優化成爲了可能。

webpack的tree shaking優化的基礎就是import的靜態導入。

require加載過程?

在node中使用requireexports時我們發現不管是在模塊中還是在全局對象上,都不存在這兩個方法,那麼這兩個方法是從何而來呢?

其實require方法本身是定義在Module中的,node在編譯階段將js文件包裝在函數將其包裝成模塊:

(function (exports, require, module, __filename, __dirname) {
    file_content...
})

使用require加載模塊的過程

  1. 根據require的參數計算絕對路徑path
  2. 根據path查找是否有緩存var cache = Module._cache[path],如果有緩存直接return
  3. 判斷是否是內置模塊如http,如果是直接return
  4. 生成模塊實例,並緩存
      var module = new Module(path, parent);
      Module._cache[path] = module;
  1. 加載模塊module.load(path);

你知道哪些前端安全問題,如何避免?

1、XSS攻擊

  • 反射型XSS

攻擊步驟

  1. 攻擊者構造出帶有惡意代碼的URL,誘導用戶點擊
  2. 服務端取出惡意代碼並拼接在html中返回給瀏覽器
  3. 瀏覽器執行惡意代碼,攻擊者獲取用戶信息或冒充用戶行爲進行攻擊

eg:

//惡意URL: http://xxx.com?key=<script>document.cookie</script>

//服務端拼接
<div>@{{params.key}}</div>

//最終瀏覽器執行了此惡意代碼獲取到用戶cookie
<div>
    <script>document.cookie</script>
</div>
  • 存儲型XSS

攻擊步驟

  1. 攻擊者在商品評論頁提交了惡意代碼到目標數據庫
  2. 用於訪問該商品,服務端將商品評論從數據庫取出並拼接在HTML中返回給瀏覽器
  3. 瀏覽器執行惡意代碼,攻擊者獲取用戶信息或冒充用戶行爲進行攻擊
存儲型XSS會將惡意代碼保存在數據庫中,會影響到所有使用的用戶,相比於反射型XSS造成後果更嚴重。
  • DOM型XSS
DOM型XSS針對的主要是前端JS代碼的漏洞

攻擊步驟

  1. 攻擊者提供帶有惡意代碼的URL,或者已經存在於數據庫的惡意代碼
  2. 瀏覽器從URL中獲取惡意代碼,或者從後端接口中獲取到惡意代碼
  3. 前端Javascript直接執行了這些惡意代碼,造成DOM型XSS攻擊

eg:

//惡意URL: http://xxx.com?key=document.cookie

//前端取出key字段並執行
evel(location.key)

防範存儲和反射型XSS

  1. 前端渲染HTML,數據從接口中獲取
  2. 在服務端拼接HTML時轉義

防範DOM型XSS

  1. 小心innerHTML,outerHTML、eval等方法
  2. 小心setTimeout等能將字符串直接執行的方法

2、CSRF攻擊

CSRF攻擊實際上是利用了瀏覽器在向A域名發起請求前,會cookie列表中查詢是否存在A域名的cookie。若是存在,則會將cookie添加至請求頭中的機制。

這個機制不在乎請求具體是從哪個域名發出,它只是關心目標路由。

攻擊步驟

  1. 用戶訪問正規網站WebA,瀏覽器保存下WebA爲此用戶設置的cookie
  2. 攻擊者誘導用戶點擊不安全的網站WebB,此時從惡意網站WebBWebA發送的請求中已經帶上了用戶的cookie

防範CSRF攻擊

  1. 如果是Ajax跨域請求,在Access-Control-Allow-Origin中設置安全的域名,如WebA的域名。
  2. 如果是form表單請求,後端需要驗證http的Referer字段,確保來源是安全的。
  3. 推薦使用token驗證

3、自動化腳本攻擊

羊毛黨通常使用腳本攻擊我們的線上活動,獲得非法利潤,他們通常使用刷API接口,自動刷單等方式獲取利潤。

通常來說,我們需要人機識別來防範腳本攻擊,在web前端服務端之間,添加一層風控系統,用於鑑別終端是否是機器。
image.png

但前端依然可以爲羊毛黨增加一些收入難度,想要薅羊毛先得過前端這一關(紙老虎)。

  1. token校驗,前端通過加密算法生成token,由風控系統校驗token,攻擊者必須破解js生成token的算法才能進行下一步。
  2. 代碼壓縮和混淆,這裏根據實際情設置混淆級別,太高級別的混淆可能會影響JS本身的執行效率。高級混淆後的代碼能防止攻擊者斷點調試
  3. 收集用戶行爲,記錄用戶進入頁面中行爲,加密後交給風控系統(風控系統通過大數據分析地理位置、ip地址、行爲數據等進行人機識別分析)
tips:在前端的加密過程中,我們可以使用一些DOM、BOM,API,因爲攻擊者通過API攻擊無法直接模擬出真實瀏覽器的環境,就算模擬也需要費一番功夫,加大攻擊者破解算法難度。

HTTPS如何實現安全加密傳輸?

  1. 客戶端發起請求,鏈接到服務器443端口
  2. 服務端的證書(自己製作或向三方機構申請),自己製作的證書需要客戶端驗證通過(用戶點一下)。證書中包含了兩個密鑰,一個公鑰一個私鑰。
  3. 服務端將公鑰返回到客戶端,公鑰中包含了證書頒發機構,證書過期時間等信息。
  4. 客戶端收到公鑰後,通過SSl/TSL層首先對公鑰信息進行驗證,如頒發機構過期時間等,如果發現異常,則會彈出一個警告框,提示證書存在問題。否則就生成一個隨機值,然後使用公鑰對此隨機值進行加密,此加密信息只能通過服務端的私鑰才能解密獲取生成的隨機值。
  5. 服務端獲取到加密信息後使用私鑰解密獲得隨機值,以後服務端和客戶端的通訊都會使用此隨機值進行加密,而這個時候,只有服務端和客戶端才知道這個隨機值(私鑰),服務端將要返回給客戶端的數據通過隨機值加密後返回。
  6. 客戶端用之前生成的隨機值解密服務段傳過來的信息,於是獲取瞭解密後的內容,整個過程第三方即使監聽到了數據,也束手無策。

HTTP/2如果實現首部壓縮?

HTTP/2通過維護靜態字典和動態字典的方式來壓縮首部

  • 靜態字典中包含了常見的頭部名稱或者頭部名稱和值的組合,如method:GET
  • 動態字典中包含了每個請求特有的鍵值對,如自定義的頭信息,針對每個TCP connection,都需要維護一份動態字典。
  1. 對於靜態字典中匹配的頭部名稱或頭部名稱和值的組合,可以使用一個字符表示,如建立連接時:

    method:GET 可以使用 1表示 (完全匹配)
    cookeie: xxx 可以使用 2:xxx表示 (頭部匹配)

  2. 同時將cookeie: xxx加入動態字典中,這樣後續的整個cookie鍵值對都可以使用一個字符表示:

    cookeie: xxx 可以使用 3表示 (加入到動態字典)

  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
}
最終我們對遞歸的優化就是放棄了使用遞歸😃
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章