社招前端二面面試題總結

腹黑的可樂發表於2023-02-28

程式碼輸出結果

var A = {n: 4399};
var B =  function(){this.n = 9999};
var C =  function(){var n = 8888};
B.prototype = A;
C.prototype = A;
var b = new B();
var c = new C();
A.n++
console.log(b.n);
console.log(c.n);

輸出結果:9999 4400

解析:

  1. console.log(b.n),在查詢b.n是首先查詢 b 物件自身有沒有 n 屬性,如果沒有會去原型(prototype)上查詢,當執行var b = new B()時,函式內部this.n=9999(此時this指向 b) 返回b物件,b物件有自身的n屬性,所以返回 9999。
  2. console.log(c.n),同理,當執行var c = new C()時,c物件沒有自身的n屬性,向上查詢,找到原型 (prototype)上的 n 屬性,因為 A.n++(此時物件A中的n為4400), 所以返回4400。

程式碼輸出結果

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('resolve3');
    console.log('timer1')
  }, 0)
  resolve('resovle1');
  resolve('resolve2');
}).then(res => {
  console.log(res)  // resolve1
  setTimeout(() => {
    console.log(p1)
  }, 1000)
}).finally(res => {
  console.log('finally', res)
})

執行結果為如下:

resolve1
finally  undefined
timer1
Promise{<resolved>: undefined}

需要注意的是最後一個定時器列印出的p1其實是.finally的返回值,我們知道.finally的返回值如果在沒有丟擲錯誤的情況下預設會是上一個Promise的返回值,而這道題中.finally上一個Promise是.then(),但是這個.then()並沒有返回值,所以p1列印出來的Promise的值會是undefined,如果在定時器的下面加上一個return 1,則值就會變成1。

瀏覽器的主要組成部分

  • ⽤戶界⾯ 包括位址列、前進/後退按鈕、書籤選單等。除了瀏覽器主窗⼝顯示的您請求的⻚⾯外,其他顯示的各個部分都屬於⽤戶界⾯。
  • 瀏覽器引擎 在⽤戶界⾯和呈現引擎之間傳送指令。
  • 呈現引擎 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在螢幕上。
  • ⽹絡 ⽤於⽹絡調⽤,⽐如 HTTP 請求。其接⼝與平臺⽆關,併為所有平臺提供底層實現。
  • ⽤戶界⾯後端 ⽤於繪製基本的窗⼝⼩部件,⽐如組合框和窗⼝。其公開了與平臺⽆關的通⽤接⼝,⽽在底層使⽤作業系統的⽤戶界⾯⽅法。
  • JavaScript 直譯器。⽤於解析和執⾏ JavaScript 程式碼。
  • 資料儲存 這是持久層。瀏覽器需要在硬碟上儲存各種資料,例如 Cookie。新的 HTML 規範 (HTML5) 定義了“⽹絡資料庫”,這是⼀個完整(但是輕便)的瀏覽器內資料庫。

值得注意的是,和⼤多數瀏覽器不同,Chrome 瀏覽器的每個標籤⻚都分別對應⼀個呈現引擎例項。每個標籤⻚都是⼀個獨⽴的程式。

什麼是 CSRF 攻擊?

(1)概念

CSRF 攻擊指的是跨站請求偽造攻擊,攻擊者誘導使用者進入一個第三方網站,然後該網站向被攻擊網站傳送跨站請求。如果使用者在被攻擊網站中儲存了登入狀態,那麼攻擊者就可以利用這個登入狀態,繞過後臺的使用者驗證,冒充使用者向伺服器執行一些操作。

CSRF 攻擊的本質是利用 cookie 會在同源請求中攜帶傳送給伺服器的特點,以此來實現使用者的冒充。

(2)攻擊型別

常見的 CSRF 攻擊有三種:

  • GET 型別的 CSRF 攻擊,比如在網站中的一個 img 標籤裡構建一個請求,當使用者開啟這個網站的時候就會自動發起提交。
  • POST 型別的 CSRF 攻擊,比如構建一個表單,然後隱藏它,當使用者進入頁面時,自動提交這個表單。
  • 連結型別的 CSRF 攻擊,比如在 a 標籤的 href 屬性裡構建一個請求,然後誘導使用者去點選。

vuex

vuex是一個專為vue.js應用程式開發的狀態管理器,它採用集中式儲存管理應用的所有元件的狀態,並且以相
應的規則保證狀態以一種可以預測的方式發生變化。

state: vuex使用單一狀態樹,用一個物件就包含來全部的應用層級狀態

mutation: 更改vuex中state的狀態的唯一方法就是提交mutation

action: action提交的是mutation,而不是直接變更狀態,action可以包含任意非同步操作

getter: 相當於vue中的computed計算屬性

函式柯里化

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

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
}

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

說一下JSON.stringify有什麼缺點?

1.如果obj裡面有時間物件,則JSON.stringify後再JSON.parse的結果,時間將只是字串的形式,而不是物件的形式
2.如果obj裡有RegExp(正規表示式的縮寫)、Error物件,則序列化的結果將只得到空物件;
3、如果obj裡有函式,undefined,則序列化的結果會把函式或 undefined丟失;
4、如果obj裡有NaN、Infinity和-Infinity,則序列化的結果會變成null
5、JSON.stringify()只能序列化物件的可列舉的自有屬性,例如 如果obj中的物件是有建構函式生成的, 則使用JSON.parse(JSON.stringify(obj))深複製後,會丟棄物件的constructor;
6、如果物件中存在迴圈引用的情況也無法正確實現深複製;

Promise.race

描述:只要promises中有一個率先改變狀態,就返回這個率先改變的Promise例項的返回值。

實現

Promise.race = function(promises){
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            promises.forEach((item) => {
                Promise.resolve(item).then(
                    value => resolve(value), 
                    reason => reject(reason)
                );
            })
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

陣列扁平化

ES5 遞迴寫法 —— isArray()、concat()

function flat11(arr) {
    var res = [];
    for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            res = res.concat(flat11(arr[i]));
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}

如果想實現第二個引數(指定“拉平”的層數),可以這樣實現,後面的幾種可以自己類似實現:

function flat(arr, level = 1) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        if(Array.isArray(arr[i]) || level >= 1) {
            res = res.concat(flat(arr[i]), level - 1);
        }
        else {
            res.push(arr[i]);
        }
    }
    return res;
}

ES6 遞迴寫法 — reduce()、concat()、isArray()

function flat(arr) {
    return arr.reduce(
        (pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []
    );
}

ES6 迭代寫法 — 擴充套件運運算元(...)、some()、concat()、isArray()

ES6 的擴充套件運運算元(...) 只能扁平化一層

function flat(arr) {
    return [].concat(...arr);
}

全部扁平化:遍歷原陣列,若arr中含有陣列則使用一次擴充套件運運算元,直至沒有為止。

function flat(arr) {
    while(arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

toString/join & split

呼叫陣列的 toString()/join() 方法(它會自動扁平化處理),將陣列變為字串然後再用 split 分割還原為陣列。由於 split 分割後形成的陣列的每一項值為字串,所以需要用一個map方法遍歷陣列將其每一項轉換為數值型。

function flat(arr){
    return arr.toString().split(',').map(item => Number(item));
    // return arr.join().split(',').map(item => Number(item));
}

使用正則

JSON.stringify(arr).replace(/[|]/g, '') 會先將陣列arr序列化為字串,然後使用 replace() 方法將字串中所有的[] 替換成空字元,從而達到扁平化處理,此時的結果為 arr 不包含 [] 的字串。最後透過JSON.parse() 解析字串。

function flat(arr) {
    return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') + "]");
}

類陣列轉化為陣列

類陣列是具有 length 屬性,但不具有陣列原型上的方法。常見的類陣列有 arguments、DOM 操作方法返回的結果(如document.querySelectorAll('div'))等。

擴充套件運運算元(...)

注意:擴充套件運運算元只能作用於 iterable 物件,即擁有 Symbol(Symbol.iterator) 屬性值。

let arr = [...arrayLike]

Array.from()

let arr = Array.from(arrayLike);

Array.prototype.slice.call()

let arr = Array.prototype.slice.call(arrayLike);

Array.apply()

let arr = Array.apply(null, arrayLike);

concat + apply

let arr = Array.prototype.concat.apply([], arrayLike);

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

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(name, fn) {
        if (this.cache[name]) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }
    off(name, fn) {
        let tasks = this.cache[name]
        if (tasks) {
            const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {
                tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {
        if (this.cache[name]) {
            // 建立副本,如果回撥函式內繼續註冊相同事件,會造成死迴圈
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}

// 測試
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
    console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
    console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布蘭', 12)
// '布蘭 12'
// 'hello, 布蘭 12'

說一下怎麼把類陣列轉換為陣列?

//透過call呼叫陣列的slice方法來實現轉換
Array.prototype.slice.call(arrayLike)

//透過call呼叫陣列的splice方法來實現轉換
Array.prototype.splice.call(arrayLike,0)

//透過apply呼叫陣列的concat方法來實現轉換
Array.prototype.concat.apply([],arrayLike)

//透過Array.from方法來實現轉換
Array.from(arrayLike)

寄生組合繼承

題目描述:實現一個你認為不錯的 js 繼承方式

實現程式碼如下:

function Parent(name) {
  this.name = name;
  this.say = () => {
    console.log(111);
  };
}
Parent.prototype.play = () => {
  console.log(222);
};
function Children(name) {
  Parent.call(this);
  this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
// let child = new Children("111");
// // console.log(child.name);
// // child.say();
// // child.play();

實現一個物件的 flatten 方法

題目描述:

const obj = {
 a: {
        b: 1,
        c: 2,
        d: {e: 5}
    },
 b: [1, 3, {a: 2, b: 3}],
 c: 3
}

flatten(obj) 結果返回如下
// {
//  'a.b': 1,
//  'a.c': 2,
//  'a.d.e': 5,
//  'b[0]': 1,
//  'b[1]': 3,
//  'b[2].a': 2,
//  'b[2].b': 3
//   c: 3
// }

實現程式碼如下:

function isObject(val) {
  return typeof val === "object" && val !== null;
}

function flatten(obj) {
  if (!isObject(obj)) {
    return;
  }
  let res = {};
  const dfs = (cur, prefix) => {
    if (isObject(cur)) {
      if (Array.isArray(cur)) {
        cur.forEach((item, index) => {
          dfs(item, `${prefix}[${index}]`);
        });
      } else {
        for (let k in cur) {
          dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
        }
      }
    } else {
      res[prefix] = cur;
    }
  };
  dfs(obj, "");

  return res;
}
flatten();

快排--時間複雜度 nlogn~ n^2 之間

題目描述:實現一個快排

實現程式碼如下:

function quickSort(arr) {
  if (arr.length < 2) {
    return arr;
  }
  const cur = arr[arr.length - 1];
  const left = arr.filter((v, i) => v <= cur && i !== arr.length - 1);
  const right = arr.filter((v) => v > cur);
  return [...quickSort(left), cur, ...quickSort(right)];
}
// console.log(quickSort([3, 6, 2, 4, 1]));

計算屬性和watch有什麼區別?以及它們的運用場景?

// 區別
  computed 計算屬性:依賴其它屬性值,並且computed的值有快取,只有它依賴的屬性值發生改變,下一次獲取computed的值時才會重新計算computed的值。
  watch 偵聽器:更多的是觀察的作用,無快取性,類似與某些資料的監聽回撥,每當監聽的資料變化時都會執行回撥進行後續操作
//運用場景
  當需要進行數值計算,並且依賴與其它資料時,應該使用computed,因為可以利用computed的快取屬性,避免每次獲取值時都要重新計算。
  當需要在資料變化時執行非同步或開銷較大的操作時,應該使用watch,使用watch選項允許執行非同步操作(訪問一個API),限制執行該操作的頻率,並在得到最終結果前,設定中間狀態。這些都是計算屬性無法做到的。

Promise.allSettled

描述:等到所有promise都返回結果,就返回一個promise例項。

實現

Promise.allSettled = function(promises) {
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            let result = [];
            let count = 0;
            promises.forEach((item, index) => {
                Promise.resolve(item).then(
                    value => {
                        count++;
                        result[index] = {
                            status: 'fulfilled',
                            value: value
                        };
                        if(count === promises.length) resolve(result);
                    }, 
                    reason => {
                        count++;
                        result[index] = {
                            status: 'rejected'.
                            reason: reason
                        };
                        if(count === promises.length) resolve(result);
                    }
                );
            });
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

正向代理和反向代理的區別

  • 正向代理:

客戶端想獲得一個伺服器的資料,但是因為種種原因無法直接獲取。於是客戶端設定了一個代理伺服器,並且指定目標伺服器,之後代理伺服器向目標伺服器轉交請求並將獲得的內容傳送給客戶端。這樣本質上起到了對真實伺服器隱藏真實客戶端的目的。實現正向代理需要修改客戶端,比如修改瀏覽器配置。

  • 反向代理:

伺服器為了能夠將工作負載分不到多個伺服器來提高網站效能 (負載均衡)等目的,當其受到請求後,會首先根據轉發規則來確定請求應該被轉發到哪個伺服器上,然後將請求轉發到對應的真實伺服器上。這樣本質上起到了對客戶端隱藏真實伺服器的作用。
一般使用反向代理後,需要透過修改 DNS 讓域名解析到代理伺服器 IP,這時瀏覽器無法察覺到真正伺服器的存在,當然也就不需要修改配置了。

正向代理和反向代理的結構是一樣的,都是 client-proxy-server 的結構,它們主要的區別就在於中間這個 proxy 是哪一方設定的。在正向代理中,proxy 是 client 設定的,用來隱藏 client;而在反向代理中,proxy 是 server 設定的,用來隱藏 server。

TCP的可靠傳輸機制

TCP 的可靠傳輸機制是基於連續 ARQ 協議和滑動視窗協議的。

TCP 協議在傳送方維持了一個傳送視窗,傳送視窗以前的報文段是已經傳送並確認了的報文段,傳送視窗中包含了已經傳送但 未確認的報文段和允許傳送但還未傳送的報文段,傳送視窗以後的報文段是快取中還不允許傳送的報文段。當傳送方向接收方發 送報文時,會依次傳送視窗內的所有報文段,並且設定一個定時器,這個定時器可以理解為是最早傳送但未收到確認的報文段。 如果在定時器的時間內收到某一個報文段的確認回答,則滑動視窗,將視窗的首部向後滑動到確認報文段的後一個位置,此時如 果還有已傳送但沒有確認的報文段,則重新設定定時器,如果沒有了則關閉定時器。如果定時器超時,則重新傳送所有已經傳送 但還未收到確認的報文段,並將超時的間隔設定為以前的兩倍。當傳送方收到接收方的三個冗餘的確認應答後,這是一種指示, 說明該報文段以後的報文段很有可能發生丟失了,那麼傳送方會啟用快速重傳的機制,就是當前定時器結束前,傳送所有的已發 送但確認的報文段。

接收方使用的是累計確認的機制,對於所有按序到達的報文段,接收方返回一個報文段的肯定回答。如果收到了一個亂序的報文 段,那麼接方會直接丟棄,並返回一個最近的按序到達的報文段的肯定回答。使用累計確認保證了返回的確認號之前的報文段都 已經按序到達了,所以傳送視窗可以移動到已確認報文段的後面。

傳送視窗的大小是變化的,它是由接收視窗剩餘大小和網路中擁塞程度來決定的,TCP 就是透過控制傳送視窗的長度來控制報文 段的傳送速率。

但是 TCP 協議並不完全和滑動視窗協議相同,因為許多的 TCP 實現會將失序的報文段給快取起來,並且發生重傳時,只會重 傳一個報文段,因此 TCP 協議的可靠傳輸機制更像是視窗滑動協議和選擇重傳協議的一個混合體。

對 CSS 工程化的理解

CSS 工程化是為瞭解決以下問題:

  1. 宏觀設計:CSS 程式碼如何組織、如何拆分、模組結構怎樣設計?
  2. 編碼最佳化:怎樣寫出更好的 CSS?
  3. 構建:如何處理我的 CSS,才能讓它的打包結果最優?
  4. 可維護性:程式碼寫完了,如何最小化它後續的變更成本?如何確保任何一個同事都能輕鬆接手?

以下三個方向都是時下比較流行的、普適性非常好的 CSS 工程化實踐:

  • 前處理器:Less、 Sass 等;
  • 重要的工程化外掛: PostCss;
  • Webpack loader 等 。

基於這三個方向,可以衍生出一些具有典型意義的子問題,這裡我們逐個來看:

(1)前處理器:為什麼要用前處理器?它的出現是為瞭解決什麼問題?

前處理器,其實就是 CSS 世界的“輪子”。前處理器支援我們寫一種類似 CSS、但實際並不是 CSS 的語言,然後把它編譯成 CSS 程式碼: 那為什麼寫 CSS 程式碼寫得好好的,偏偏要轉去寫“類 CSS”呢?這就和本來用 JS 也可以實現所有功能,但最後卻寫 React 的 jsx 或者 Vue 的模板語法一樣——為了爽!要想知道有了前處理器有多爽,首先要知道的是傳統 CSS 有多不爽。隨著前端業務複雜度的提高,前端工程中對 CSS 提出了以下的訴求:

  1. 宏觀設計上:我們希望能最佳化 CSS 檔案的目錄結構,對現有的 CSS 檔案實現複用;
  2. 編碼最佳化上:我們希望能寫出結構清晰、簡明易懂的 CSS,需要它具有一目瞭然的巢狀層級關係,而不是無差別的一鋪到底寫法;我們希望它具有變數特徵、計算能力、迴圈能力等等更強的可程式設計性,這樣我們可以少寫一些無用的程式碼;
  3. 可維護性上:更強的可程式設計性意味著更優質的程式碼結構,實現複用意味著更簡單的目錄結構和更強的擴充能力,這兩點如果能做到,自然會帶來更強的可維護性。

這三點是傳統 CSS 所做不到的,也正是前處理器所解決掉的問題。前處理器普遍會具備這樣的特性:

  • 巢狀程式碼的能力,透過巢狀來反映不同 css 屬性之間的層級關係 ;
  • 支援定義 css 變數;
  • 提供計算函式;
  • 允許對程式碼片段進行 extend 和 mixin;
  • 支援迴圈語句的使用;
  • 支援將 CSS 檔案模組化,實現複用。

(2)PostCss:PostCss 是如何工作的?我們在什麼場景下會使用 PostCss?

它和前處理器的不同就在於,前處理器處理的是 類CSS,而 PostCss 處理的就是 CSS 本身。Babel 可以將高版本的 JS 程式碼轉換為低版本的 JS 程式碼。PostCss 做的是類似的事情:它可以編譯尚未被瀏覽器廣泛支援的先進的 CSS 語法,還可以自動為一些需要額外相容的語法增加字首。更強的是,由於 PostCss 有著強大的外掛機制,支援各種各樣的擴充套件,極大地強化了 CSS 的能力。

PostCss 在業務中的使用場景非常多:

  • 提高 CSS 程式碼的可讀性:PostCss 其實可以做類似前處理器能做的工作;
  • 當我們的 CSS 程式碼需要適配低版本瀏覽器時,PostCss 的 Autoprefixer 外掛可以幫助我們自動增加瀏覽器字首;
  • 允許我們編寫面向未來的 CSS:PostCss 能夠幫助我們編譯 CSS next 程式碼;

(3)Webpack 能處理 CSS 嗎?如何實現? Webpack 能處理 CSS 嗎:

  • Webpack 在裸奔的狀態下,是不能處理 CSS 的,Webpack 本身是一個面向 JavaScript 且只能處理 JavaScript 程式碼的模組化打包工具;
  • Webpack 在 loader 的輔助下,是可以處理 CSS 的。

如何用 Webpack 實現對 CSS 的處理:

  • Webpack 中操作 CSS 需要使用的兩個關鍵的 loader:css-loader 和 style-loader
  • 注意,答出“用什麼”有時候可能還不夠,面試官會懷疑你是不是在背答案,所以你還需要了解每個 loader 都做了什麼事情:

    • css-loader:匯入 CSS 模組,對 CSS 程式碼進行編譯處理;
    • style-loader:建立style標籤,把 CSS 內容寫入標籤。

在實際使用中,css-loader 的執行順序一定要安排在 style-loader 的前面。因為只有完成了編譯過程,才可以對 css 程式碼進行插入;若提前插入了未編譯的程式碼,那麼 webpack 是無法理解這坨東西的,它會無情報錯。

CSS選擇器及其優先順序

選擇器格式優先順序權重
id選擇器#id100
類選擇器#classname10
屬性選擇器a[ref=“eee”]10
偽類選擇器li:last-child10
標籤選擇器div1
偽元素選擇器li:after1
相鄰兄弟選擇器h1+p0
子選擇器ul>li0
後代選擇器li a0
萬用字元選擇器*0

對於選擇器的優先順序

  • 標籤選擇器、偽元素選擇器:1
  • 類選擇器、偽類選擇器、屬性選擇器:10
  • id 選擇器:100
  • 內聯樣式:1000

注意事項:

  • !important宣告的樣式的優先順序最高;
  • 如果優先順序相同,則最後出現的樣式生效;
  • 繼承得到的樣式的優先順序最低;
  • 通用選擇器(*)、子選擇器(>)和相鄰同胞選擇器(+)並不在這四個等級中,所以它們的權值都為 0 ;
  • 樣式表的來源不同時,優先順序順序為:內聯樣式 > 內部樣式 > 外部樣式 > 瀏覽器使用者自定義樣式 > 瀏覽器預設樣式。

相關文章