2023秋招前端面試必會的面試題

coder2028發表於2023-03-06

判斷陣列的方式有哪些

  • 透過Object.prototype.toString.call()做判斷
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 透過原型鏈做判斷
obj.__proto__ === Array.prototype;
  • 透過ES6的Array.isArray()做判斷
Array.isArrray(obj);
  • 透過instanceof做判斷
obj instanceof Array
  • 透過Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

JS 整數是怎麼表示的?

  • 透過 Number 型別來表示,遵循 IEEE754 標準,透過 64 位來表示一個數字,(1 + 11 + 52),最大安全數字是 Math.pow(2, 53) - 1,對於 16 位十進位制。(符號位 + 指數位 + 小數部分有效位)

實現 LazyMan

題目描述:

實現一個LazyMan,可以按照以下方式呼叫:
LazyMan(“Hank”)輸出:
Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)輸出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)輸出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).eat(“supper”).sleepFirst(5)輸出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

實現程式碼如下:

class _LazyMan {
  constructor(name) {
    this.tasks = [];
    const task = () => {
      console.log(`Hi! This is ${name}`);
      this.next();
    };
    this.tasks.push(task);
    setTimeout(() => {
      // 把 this.next() 放到呼叫棧清空之後執行
      this.next();
    }, 0);
  }
  next() {
    const task = this.tasks.shift(); // 取第一個任務執行
    task && task();
  }
  sleep(time) {
    this._sleepWrapper(time, false);
    return this; // 鏈式呼叫
  }
  sleepFirst(time) {
    this._sleepWrapper(time, true);
    return this;
  }
  _sleepWrapper(time, first) {
    const task = () => {
      setTimeout(() => {
        console.log(`Wake up after ${time}`);
        this.next();
      }, time * 1000);
    };
    if (first) {
      this.tasks.unshift(task); // 放到任務佇列頂部
    } else {
      this.tasks.push(task); // 放到任務佇列尾部
    }
  }
  eat(name) {
    const task = () => {
      console.log(`Eat ${name}`);
      this.next();
    };
    this.tasks.push(task);
    return this;
  }
}
function LazyMan(name) {
  return new _LazyMan(name);
}

手寫釋出訂閱

class EventListener {
    listeners = {};
    on(name, fn) {
        (this.listeners[name] || (this.listeners[name] = [])).push(fn)
    }
    once(name, fn) {
        let tem = (...args) => {
            this.removeListener(name, fn)
            fn(...args)
        }
        fn.fn = tem
        this.on(name, tem)
    }
    removeListener(name, fn) {
        if (this.listeners[name]) {
            this.listeners[name] = this.listeners[name].filter(listener => (listener != fn && listener != fn.fn))
        }
    }
    removeAllListeners(name) {
        if (name && this.listeners[name]) delete this.listeners[name]
        this.listeners = {}
    }
    emit(name, ...args) {
        if (this.listeners[name]) {
            this.listeners[name].forEach(fn => fn.call(this, ...args))
        }
    }
}

非同步任務排程器

描述:實現一個帶併發限制的非同步排程器 Scheduler,保證同時執行的任務最多有 limit 個。

實現

class Scheduler {
    queue = [];  // 用佇列儲存正在執行的任務
    runCount = 0;  // 計數正在執行的任務個數
    constructor(limit) {
        this.maxCount = limit;  // 允許併發的最大個數
    }
    add(time, data){
        const promiseCreator = () => {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    console.log(data);
                    resolve();
                }, time);
            });
        }
        this.queue.push(promiseCreator);
        // 每次新增的時候都會嘗試去執行任務
        this.request();
    }
    request() {
        // 佇列中還有任務才會被執行
        if(this.queue.length && this.runCount < this.maxCount) {
            this.runCount++;
            // 執行先加入佇列的函式
            this.queue.shift()().then(() => {
                this.runCount--;
                // 嘗試進行下一次任務
                this.request();
            });
        }
    }
}

// 測試
const scheduler = new Scheduler(2);
const addTask = (time, data) => {
    scheduler.add(time, data);
}

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 輸出結果 2 3 1 4

首屏和白屏時間如何計算

首屏時間的計算,可以由 Native WebView 提供的類似 onload 的方法實現,在 ios 下對應的是 webViewDidFinishLoad,在 android 下對應的是onPageFinished事件。

白屏的定義有多種。可以認為“沒有任何內容”是白屏,可以認為“網路或服務異常”是白屏,可以認為“資料載入中”是白屏,可以認為“圖片載入不出來”是白屏。場景不同,白屏的計算方式就不相同。

方法1:當頁面的元素數小於x時,則認為頁面白屏。比如“沒有任何內容”,可以獲取頁面的DOM節點數,判斷DOM節點數少於某個閾值X,則認為白屏。 方法2:當頁面出現業務定義的錯誤碼時,則認為是白屏。比如“網路或服務異常”。 方法3:當頁面出現業務定義的特徵值時,則認為是白屏。比如“資料載入中”。

參考 前端進階面試題詳細解答

列表轉成樹形結構

題目描述:

[
    {
        id: 1,
        text: '節點1',
        parentId: 0 //這裡用0表示為頂級節點
    },
    {
        id: 2,
        text: '節點1_1',
        parentId: 1 //透過這個欄位來確定子父級
    }
    ...
]

轉成
[
    {
        id: 1,
        text: '節點1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '節點1_1',
                parentId:1
            }
        ]
    }
]

實現程式碼如下:

function listToTree(data) {
  let temp = {};
  let treeData = [];
  for (let i = 0; i < data.length; i++) {
    temp[data[i].id] = data[i];
  }
  for (let i in temp) {
    if (+temp[i].parentId != 0) {
      if (!temp[temp[i].parentId].children) {
        temp[temp[i].parentId].children = [];
      }
      temp[temp[i].parentId].children.push(temp[i]);
    } else {
      treeData.push(temp[i]);
    }
  }
  return treeData;
}

釋出訂閱模式(事件匯流排)

描述:實現一個釋出訂閱模式,擁有 on, emit, once, off 方法

class EventEmitter {
    constructor() {
        // 包含所有監聽器函式的容器物件
        // 內部結構: {msg1: [listener1, listener2], msg2: [listener3]}
        this.cache = {};
    }
    // 實現訂閱
    on(name, callback) {
        if(this.cache[name]) {
            this.cache[name].push(callback);
        }
        else {
            this.cache[name] = [callback];
        }
    }
    // 刪除訂閱
    off(name, callback) {
        if(this.cache[name]) {
            this.cache[name] = this.cache[name].filter(item => item !== callback);
        }
        if(this.cache[name].length === 0) delete this.cache[name];
    }
    // 只執行一次訂閱事件
    once(name, callback) {
        callback();
        this.off(name, callback);
    }
    // 觸發事件
    emit(name, ...data) {
        if(this.cache[name]) {
            // 建立副本,如果回撥函式內繼續註冊相同事件,會造成死迴圈
            let tasks = this.cache[name].slice();
            for(let fn of tasks) {
                fn(...data);
            }
        }
    }
}

其他值到字串的轉換規則?

  • Null 和 Undefined 型別 ,null 轉換為 "null",undefined 轉換為 "undefined",
  • Boolean 型別,true 轉換為 "true",false 轉換為 "false"。
  • Number 型別的值直接轉換,不過那些極小和極大的數字會使用指數形式。
  • Symbol 型別的值直接轉換,但是隻允許顯式強制型別轉換,使用隱式強制型別轉換會產生錯誤。
  • 對普通物件來說,除非自行定義 toString() 方法,否則會呼叫 toString()(Object.prototype.toString())來返回內部屬性 [[Class]] 的值,如"[object Object]"。如果物件有自己的 toString() 方法,字串化時就會呼叫該方法並使用其返回值。

HTTP狀態碼

  • 1xx 資訊性狀態碼 websocket upgrade
  • 2xx 成功狀態碼

    • 200 伺服器已成功處理了請求
    • 204(沒有響應體)
    • 206(範圍請求 暫停繼續下載)
  • 3xx 重定向狀態碼

    • 301(永久) :請求的頁面已永久跳轉到新的url
    • 302(臨時) :允許各種各樣的重定向,一般情況下都會實現為到 GET 的重定向,但是不能確保 POST 會重定向為 POST
    • 303 只允許任意請求到 GET 的重定向
    • 304 未修改:自從上次請求後,請求的網頁未修改過
    • 307:307302 一樣,除了不允許 POSTGET 的重定向
  • 4xx 客戶端錯誤狀態碼

    • 400 客戶端引數錯誤
    • 401 沒有登入
    • 403 登入了沒許可權 比如管理系統
    • 404 頁面不存在
    • 405 禁用請求中指定的方法
  • 5xx 服務端錯誤狀態碼

    • 500 伺服器錯誤:伺服器內部錯誤,無法完成請求
    • 502 錯誤閘道器:伺服器作為閘道器或代理出現錯誤
    • 503 服務不可用:伺服器目前無法使用
    • 504 閘道器超時:閘道器或代理伺服器,未及時獲取請求

大數相加

題目描述:實現一個add方法完成兩個大數相加

let a = "9007199254740991";
let b = "1234567899999999999";

function add(a ,b){
   //...
}

實現程式碼如下:

function add(a ,b){
   //取兩個數字的最大長度
   let maxLength = Math.max(a.length, b.length);
   //用0去補齊長度
   a = a.padStart(maxLength , 0);//"0009007199254740991"
   b = b.padStart(maxLength , 0);//"1234567899999999999"
   //定義加法過程中需要用到的變數
   let t = 0;
   let f = 0;   //"進位"
   let sum = "";
   for(let i=maxLength-1 ; i>=0 ; i--){
      t = parseInt(a[i]) + parseInt(b[i]) + f;
      f = Math.floor(t/10);
      sum = t%10 + sum;
   }
   if(f!==0){
      sum = '' + f + sum;
   }
   return sum;
}

對Service Worker的理解

Service Worker 是執行在瀏覽器背後的獨立執行緒,一般可以用來實現快取功能。使用 Service Worker的話,傳輸協議必須為 HTTPS。因為 Service Worker 中涉及到請求攔截,所以必須使用 HTTPS 協議來保障安全。

Service Worker 實現快取功能一般分為三個步驟:首先需要先註冊 Service Worker,然後監聽到 install 事件以後就可以快取需要的檔案,那麼在下次使用者訪問的時候就可以透過攔截請求的方式查詢是否存在快取,存在快取的話就可以直接讀取快取檔案,否則就去請求資料。以下是這個步驟的實現:

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 註冊成功')
    })
    .catch(function(err) {
      console.log('servcie worker 註冊失敗')
    })
}
// sw.js
// 監聽 `install` 事件,回撥中快取所需檔案
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})
// 攔截所有請求事件
// 如果快取中已經有請求的資料就直接用快取,否則去請求資料
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

開啟頁面,可以在開發者工具中的 Application 看到 Service Worker 已經啟動了: 在 Cache 中也可以發現所需的檔案已被快取:

如何避免React生命週期中的坑

16.3版本

>=16.4版本

線上檢視https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram(opens new window)

  • 避免生命週期中的坑需要做好兩件事:不在恰當的時候呼叫了不該呼叫的程式碼;在需要呼叫時,不要忘了呼叫。
  • 那麼主要有這麼 7 種情況容易造成生命週期的坑

    • getDerivedStateFromProps 容易編寫反模式程式碼,使受控元件與非受控元件區分模糊
    • componentWillMount 在 React 中已被標記棄用,不推薦使用,主要原因是新的非同步渲染架構會導致它被多次呼叫。所以網路請求及事件繫結程式碼應移至 componentDidMount 中。
    • componentWillReceiveProps 同樣被標記棄用,被 getDerivedStateFromProps 所取代,主要原因是效能問題
    • shouldComponentUpdate 透過返回 true 或者 false 來確定是否需要觸發新的渲染。主要用於效能最佳化
    • componentWillUpdate 同樣是由於新的非同步渲染機制,而被標記廢棄,不推薦使用,原先的邏輯可結合 getSnapshotBeforeUpdatecomponentDidUpdate 改造使用。
    • 如果在 componentWillUnmount 函式中忘記解除事件繫結,取消定時器等清理操作,容易引發 bug
    • 如果沒有新增錯誤邊界處理,當渲染髮生異常時,使用者將會看到一個無法操作的白屏,所以一定要新增

“React 的請求應該放在哪裡,為什麼?” 這也是經常會被追問的問題。你可以這樣回答。

對於非同步請求,應該放在 componentDidMount 中去操作。從時間順序來看,除了 componentDidMount 還可以有以下選擇:

  • constructor:可以放,但從設計上而言不推薦。constructor 主要用於初始化 state 與函式繫結,並不承載業務邏輯。而且隨著類屬性的流行,constructor 已經很少使用了
  • componentWillMount:已被標記廢棄,在新的非同步渲染架構下會觸發多次渲染,容易引發 Bug,不利於未來 React 升級後的程式碼維護。
  • 所以React 的請求放在 componentDidMount 裡是最好的選擇

透過現象看本質:React 16 緣何兩次求變?

Fiber 架構簡析

Fiber 是 React 16 對 React 核心演演算法的一次重寫。你只需要 get 到這一個點:Fiber 會使原本同步的渲染過程變成非同步的

在 React 16 之前,每當我們觸發一次元件的更新,React 都會構建一棵新的虛擬 DOM 樹,透過與上一次的虛擬 DOM 樹進行 diff,實現對 DOM 的定向更新。這個過程,是一個遞迴的過程。下面這張圖形象地展示了這個過程的特徵:

如圖所示,同步渲染的遞迴呼叫棧是非常深的,只有最底層的呼叫返回了,整個渲染過程才會開始逐層返回。這個漫長且不可打斷的更新過程,將會帶來使用者體驗層面的巨大風險:同步渲染一旦開始,便會牢牢抓住主執行緒不放,直到遞迴徹底完成。在這個過程中,瀏覽器沒有辦法處理任何渲染之外的事情,會進入一種無法處理使用者互動的狀態。因此若渲染時間稍微長一點,頁面就會面臨卡頓甚至卡死的風險。

而 React 16 引入的 Fiber 架構,恰好能夠解決掉這個風險:Fiber 會將一個大的更新任務拆解為許多個小任務每當執行完一個小任務時,渲染執行緒都會把主執行緒交回去,看看有沒有優先順序更高的工作要處理,確保不會出現其他任務被“餓死”的情況,進而避免同步渲染帶來的卡頓。在這個過程中,渲染執行緒不再“一去不回頭”,而是可以被打斷的,這就是所謂的“非同步渲染”,它的執行過程如下圖所示:

換個角度看生命週期工作流

Fiber 架構的重要特徵就是可以被打斷的非同步渲染模式。但這個“打斷”是有原則的,根據“能否被打斷”這一標準,React 16 的生命週期被劃分為了 render 和 commit 兩個階段,而 commit 階段又被細分為了 pre-commit 和 commit。每個階段所涵蓋的生命週期如下圖所示:

我們先來看下三個階段各自有哪些特徵

  • render 階段:純淨且沒有副作用,可能會被 React 暫停、終止或重新啟動。
  • pre-commit 階段:可以讀取 DOM。
  • commit 階段:可以使用 DOM,執行副作用,安排更新。

總的來說,render 階段在執行過程中允許被打斷,而 commit 階段則總是同步執行的。

為什麼這樣設計呢?簡單來說,由於 render 階段的操作對使用者來說其實是“不可見”的,所以就算打斷再重啟,對使用者來說也是零感知。而 commit 階段的操作則涉及真實 DOM 的渲染,所以這個過程必須用同步渲染來求穩

函式柯里化

什麼叫函式柯里化?其實就是將使用多個引數的函式轉換成一系列使用一個引數的函式的技術。還不懂?來舉個例子。

function add(a, b, c) {
    return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)

現在就是要實現 curry 這個函式,使函式從一次呼叫傳入多個引數變成多次呼叫每次傳一個引數。

function curry(fn) {
    let judge = (...args) => {
        if (args.length == fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}

陣列去重

實現程式碼如下:

function uniqueArr(arr) {
  return [...new Set(arr)];
}

對虛擬DOM的理解

虛擬dom從來不是用來和直接操作dom對比的,它們倆最終殊途同歸。虛擬dom只不過是區域性更新的一個環節而已,整個環節的對比物件是全量更新。虛擬dom對於state=UI的意義是,虛擬dom使diff成為可能(理論上也可以直接用dom物件diff,但是太臃腫),促進了新的開發思想,又不至於效能太差。但是效能再好也不可能好過直接操作dom,人腦連diff都省了。還有一個很重要的意義是,對檢視抽象,為跨平臺助力

其實我最終希望你明白的事情只有一件:虛擬 DOM 的價值不在效能,而在別處。因此想要從效能角度來把握虛擬 DOM 的優勢,無異於南轅北轍。偏偏在面試場景下,10 個人裡面有 9 個都走這條歧路,最後9個人裡面自然沒有一個能自圓其說,實在讓人惋惜。

為什麼需要瀏覽器快取?

對於瀏覽器的快取,主要針對的是前端的靜態資源,最好的效果就是,在發起請求之後,拉取相應的靜態資源,並儲存在本地。如果伺服器的靜態資源沒有更新,那麼在下次請求的時候,就直接從本地讀取即可,如果伺服器的靜態資源已經更新,那麼我們再次請求的時候,就到伺服器拉取新的資源,並儲存在本地。這樣就大大的減少了請求的次數,提高了網站的效能。這就要用到瀏覽器的快取策略了。

所謂的瀏覽器快取指的是瀏覽器將使用者請求過的靜態資源,儲存到電腦本地磁碟中,當瀏覽器再次訪問時,就可以直接從本地載入,不需要再去服務端請求了。

使用瀏覽器快取,有以下優點:

  • 減少了伺服器的負擔,提高了網站的效能
  • 加快了客戶端網頁的載入速度
  • 減少了多餘網路資料傳輸

介紹一下 webpack scope hosting

作用域提升,將分散的模組劃分到同一個作用域中,避免了程式碼的重複引入,有效減少打包後的程式碼體積和執行時的記憶體損耗;

常見的瀏覽器核心比較

  • Trident: 這種瀏覽器核心是 IE 瀏覽器用的核心,因為在早期 IE 佔有大量的市場份額,所以這種核心比較流行,以前有很多網頁也是根據這個核心的標準來編寫的,但是實際上這個核心對真正的網頁標準支援不是很好。但是由於 IE 的高市場佔有率,微軟也很長時間沒有更新 Trident 核心,就導致了 Trident 核心和 W3C 標準脫節。還有就是 Trident 核心的大量 Bug 等安全問題沒有得到解決,加上一些專家學者公開自己認為 IE 瀏覽器不安全的觀點,使很多使用者開始轉向其他瀏覽器。
  • Gecko: 這是 Firefox 和 Flock 所採用的核心,這個核心的優點就是功能強大、豐富,可以支援很多複雜網頁效果和瀏覽器擴充套件介面,但是代價是也顯而易見就是要消耗很多的資源,比如記憶體。
  • Presto: Opera 曾經採用的就是 Presto 核心,Presto 核心被稱為公認的瀏覽網頁速度最快的核心,這得益於它在開發時的天生優勢,在處理 JS 指令碼等指令碼語言時,會比其他的核心快3倍左右,缺點就是為了達到很快的速度而丟掉了一部分網頁相容性。
  • Webkit: Webkit 是 Safari 採用的核心,它的優點就是網頁瀏覽速度較快,雖然不及 Presto 但是也勝於 Gecko 和 Trident,缺點是對於網頁程式碼的容錯性不高,也就是說對網頁程式碼的相容性較低,會使一些編寫不標準的網頁無法正確顯示。WebKit 前身是 KDE 小組的 KHTML 引擎,可以說 WebKit 是 KHTML 的一個開源的分支。
  • Blink: 谷歌在 Chromium Blog 上發表部落格,稱將與蘋果的開源瀏覽器核心 Webkit 分道揚鑣,在 Chromium 專案中研發 Blink 渲染引擎(即瀏覽器核心),內建於 Chrome 瀏覽器之中。其實 Blink 引擎就是 Webkit 的一個分支,就像 webkit 是KHTML 的分支一樣。Blink 引擎現在是谷歌公司與 Opera Software 共同研發,上面提到過的,Opera 棄用了自己的 Presto 核心,加入 Google 陣營,跟隨谷歌一起研發 Blink。

陣列扁平化

題目描述:實現一個方法使多維陣列變成一維陣列

最常見的遞迴版本如下:

function flatter(arr) {
  if (!arr.length) return;
  return arr.reduce(
    (pre, cur) =>
      Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
    []
  );
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
擴充套件思考:能用迭代的思路去實現嗎?

實現程式碼如下:

function flatter(arr) {
  if (!arr.length) return;
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

相關文章