【半月刊 4】前端高頻面試題及答案彙總

木易楊說發表於2019-04-15

引言

半月刊第四期來啦,這段時間 Daily-Interview-Question 新增了 14 道高頻面試題,今天就把最近半月彙總的面試題和部分答案發給大家,幫助大家查漏補缺,歡迎 加群 互相學習。

更多更全的面試題和答案彙總在下面的專案中,點選檢視。

專案地址:Daily-Interview-Question

第 40 題:在 Vue 中,子元件為何不可以修改父元件傳遞的 Prop

如果修改了,Vue 是如何監控到屬性的修改並給出警告的。

解析:

  1. 子元件為何不可以修改父元件傳遞的 Prop 單向資料流,易於監測資料的流動,出現了錯誤可以更加迅速的定位到錯誤發生的位置。
  2. 如果修改了,Vue 是如何監控到屬性的修改並給出警告的。
if (process.env.NODE_ENV !== 'production') {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }
複製程式碼

在initProps的時候,在defineReactive時通過判斷是否在開發環境,如果是開發環境,會在觸發set的時候判斷是否此key是否處於updatingChildren中被修改,如果不是,說明此修改來自子元件,觸發warning提示。

需要特別注意的是,當你從子元件修改的prop屬於基礎型別時會觸發提示。 這種情況下,你是無法修改父元件的資料來源的, 因為基礎型別賦值時是值拷貝。你直接將另一個非基礎型別(Object, array)賦值到此key時也會觸發提示(但實際上不會影響父元件的資料來源), 當你修改object的屬性時不會觸發提示,並且會修改父元件資料來源的資料。

未完待續,點選檢視更多細節:第 40 題

第 41 題:下面程式碼輸出什麼

var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()
複製程式碼

解析:

依次輸出:undefined -> 10 -> 20

在立即執行函式中,var a = 20; 語句定義了一個區域性變數 a,由於js的變數宣告提升機制,區域性變數a的宣告會被提升至立即執行函式的函式體最上方,且由於這樣的提升並不包括賦值,因此第一條列印語句會列印undefined,最後一條語句會列印20

由於變數宣告提升,a = 5; 這條語句執行時,區域性的變數a已經宣告,因此它產生的效果是對區域性的變數a賦值,此時window.a 依舊是最開始賦值的10

未完待續,點選檢視更多細節:第 41題

第 42 題:實現一個 sleep 函式

比如 sleep(1000) 意味著等待1000毫秒,可從 Promise、Generator、Async/Await 等角度實現。

解析:4 種方式

//Promise
const sleep = time => {
  return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
  console.log(1)
})

//Generator
function* sleepGenerator(time) {
  yield new Promise(function(resolve,reject){
    setTimeout(resolve,time);
  })
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})

//async
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();

//ES5
function sleep(callback,time) {
  if(typeof callback === 'function')
    setTimeout(callback,time)
}

function output(){
  console.log(1);
}
sleep(output,1000);
複製程式碼

未完待續,點選檢視更多細節:第 42 題

第 43 題:使用 sort() 對陣列 [3, 15, 8, 29, 102, 22] 進行排序,輸出結果

解析:

sort 函式,可以接收一個函式,返回值是比較兩個數的相對順序的值

  1. 預設沒有函式 是按照 UTF-16 排序的,對於字母數字 你可以利用 ASCII 進行記憶
 [3, 15, 8, 29, 102, 22].sort();

// [102, 15, 22, 29, 3, 8]
複製程式碼
  1. 帶函式的比較
 [3, 15, 8, 29, 102, 22].sort((a,b) => {return a - b});
複製程式碼
  • 返回值大於0 即a-b > 0 , a 和 b 交換位置
  • 返回值大於0 即a-b < 0 , a 和 b 位置不變
  • 返回值等於0 即a-b = 0 , a 和 b 位置不變

對於函式體返回 b-a 可以類比上面的返回值進行交換位置

未完待續,點選檢視更多細節:第 43 題

第 44 題:介紹 HTTPS 握手過程

解析:

開始加密通訊之前,客戶端和伺服器首先必須建立連線和交換引數,這個過程叫做握手(handshake)。

假定客戶端叫做愛麗絲,伺服器叫做鮑勃,整個握手過程可以用下圖說明。

img

握手階段分成五步。

第一步,愛麗絲給出協議版本號、一個客戶端生成的隨機數(Client random),以及客戶端支援的加密方法。

第二步,鮑勃確認雙方使用的加密方法,並給出數字證書、以及一個伺服器生成的隨機數(Server random)。

第三步,愛麗絲確認數字證書有效,然後生成一個新的隨機數(Premaster secret),並使用數字證書中的公鑰,加密這個隨機數,發給鮑勃。

第四步,鮑勃使用自己的私鑰,獲取愛麗絲髮來的隨機數(即Premaster secret)。

第五步,愛麗絲和鮑勃根據約定的加密方法,使用前面的三個隨機數,生成"對話金鑰"(session key),用來加密接下來的整個對話過程。

參考:

圖解SSL/TLS協議

SSL/TLS協議執行機制的概述

未完待續,點選檢視更多細節:第 44 題

第 45 題:HTTPS 握手過程中,客戶端如何驗證證書的合法性

解析:

  • 1、首先什麼是HTTP協議? http協議是超文字傳輸協議,位於tcp/ip四層模型中的應用層;通過請求/響應的方式在客戶端和伺服器之間進行通訊;但是缺少安全性,http協議資訊傳輸是通過明文的方式傳輸,不做任何加密,相當於在網路上裸奔;容易被中間人惡意篡改,這種行為叫做中間人攻擊;

  • 2、加密通訊: 為了安全性,雙方可以使用對稱加密的方式key進行資訊交流,但是這種方式對稱加密祕鑰也會被攔截,也不夠安全,進而還是存在被中間人攻擊風險; 於是人們又想出來另外一種方式,使用非對稱加密的方式;使用公鑰/私鑰加解密;通訊方A發起通訊並攜帶自己的公鑰,接收方B通過公鑰來加密對稱祕鑰;然後傳送給發起方A;A通過私鑰解密;雙發接下來通過對稱祕鑰來進行加密通訊;但是這種方式還是會存在一種安全性;中間人雖然不知道發起方A的私鑰,但是可以做到偷天換日,將攔截髮起方的公鑰key;並將自己生成的一對公/私鑰的公鑰傳送給B;接收方B並不知道公鑰已經被偷偷換過;按照之前的流程,B通過公鑰加密自己生成的對稱加密祕鑰key2;傳送給A; 這次通訊再次被中間人攔截,儘管後面的通訊,兩者還是用key2通訊,但是中間人已經掌握了Key2;可以進行輕鬆的加解密;還是存在被中間人攻擊風險;

  • 3、解決困境:權威的證書頒發機構CA來解決;

    • 3.1製作證書:作為服務端的A,首先把自己的公鑰key1發給證書頒發機構,向證書頒發機構進行申請證書;證書頒發機構有一套自己的公私鑰,CA通過自己的私鑰來加密key1,並且通過服務端網址等資訊生成一個證書籤名,證書籤名同樣使用機構的私鑰進行加密;製作完成後,機構將證書發給A;
    • 3.2校驗證書真偽:當B向服務端A發起請求通訊的時候,A不再直接返回自己的公鑰,而是返回一個證書; 說明:各大瀏覽器和作業系統已經維護了所有的權威證書機構的名稱和公鑰。B只需要知道是哪個權威機構發的證書,使用對應的機構公鑰,就可以解密出證書籤名;接下來,B使用同樣的規則,生成自己的證書籤名,如果兩個簽名是一致的,說明證書是有效的; 簽名驗證成功後,B就可以再次利用機構的公鑰,解密出A的公鑰key1;接下來的操作,就是和之前一樣的流程了;
    • 3.3:中間人是否會攔截髮送假證書到B呢? 因為證書的簽名是由伺服器端網址等資訊生成的,並且通過第三方機構的私鑰加密中間人無法篡改; 所以最關鍵的問題是證書籤名的真偽;
  • 4、https主要的思想是在http基礎上增加了ssl安全層,即以上認證過程。

未完待續,點選檢視更多細節:第 45 題

第 46 題:輸出以下程式碼執行的結果並解釋為什麼

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
複製程式碼

解析:

涉及知識點:

  • 類陣列(ArrayLike):

一組資料,由陣列來存,但是如果要對這組資料進行擴充套件,會影響到陣列原型,ArrayLike的出現則提供了一箇中間資料橋樑,ArrayLike有陣列的特性, 但是對ArrayLike的擴充套件並不會影響到原生的陣列。

  • push方法:

push 方法有意具有通用性。該方法和 call() 或 apply() 一起使用時,可應用在類似陣列的物件上。push 方法根據 length 屬性來決定從哪裡開始插入給定的值。如果 length 不能被轉成一個數值,則插入的元素索引為 0,包括 length 不存在時。當 length 不存在時,將會建立它。 唯一的原生類陣列(array-like)物件是 Strings,儘管如此,它們並不適用該方法,因為字串是不可改變的。

  • 物件轉陣列的方式:

Array.from()、splice()、concat()等。

題分析: 這個obj中定義了兩個key值,分別為splice和push分別對應陣列原型中的splice和push方法,因此這個obj可以呼叫陣列中的push和splice方法,呼叫物件的push方法:push(1),因為此時obj中定義length為2,所以從陣列中的第二項開始插入,也就是陣列的第三項(下表為2的那一項),因為陣列是從第0項開始的,這時已經定義了下標為2和3這兩項,所以它會替換第三項也就是下標為2的值,第一次執行push完,此時key為2的屬性值為1,同理:第二次執行push方法,key為3的屬性值為2。此時的輸出結果就是:

Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]---->
[
  2: 1,
  3: 2,
  length: 4,
  push: ƒ push(),
  splice: ƒ splice()
]
複製程式碼

因為只是定義了2和3兩項,沒有定義0和1這兩項,所以前面會是empty。 如果講這道題改為:

var obj = {
    '2': 3,
    '3': 4,
    'length': 0,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
複製程式碼

此時的列印結果就是:

Object(2) [1, 2, 2: 3, 3: 4, splice: ƒ, push: ƒ]---->
[
  0: 1,
  1: 2,
  2: 3,
  3: 4,
  length: 2,
  push: ƒ push(),
  splice: ƒ splice()
]
複製程式碼

原理:此時length長度設定為0,push方法從第0項開始插入,所以填充了第0項的empty 至於為什麼物件新增了splice屬性後並沒有呼叫就會變成類陣列物件這個問題,這是控制檯中 DevTools 猜測類陣列的一個方式: github.com/ChromeDevTo…

未完待續,點選檢視更多細節:第 46 題

第 47 題:雙向繫結和 vuex 是否衝突

解析:

當在嚴格模式中使用 Vuex 時,在屬於 Vuex 的 state 上使用 v-model 會比較棘手:

<input v-model="obj.message">
複製程式碼

假設這裡的 obj 是在計算屬性中返回的一個屬於 Vuex store 的物件,在使用者輸入時,v-model 會試圖直接修改 obj.message。在嚴格模式中,由於這個修改不是在 mutation 函式中執行的, 這裡會丟擲一個錯誤。

用“Vuex 的思維”去解決這個問題的方法是:給 <input> 中繫結 value,然後偵聽 input 或者 change 事件,在事件回撥中呼叫 action:

<input :value="message" @input="updateMessage">
複製程式碼
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}
複製程式碼

下面是 mutation 函式:

// ...
mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}
複製程式碼

雙向繫結的計算屬性

必須承認,這樣做比簡單地使用“v-model + 區域性狀態”要囉嗦得多,並且也損失了一些 v-model 中很有用的特性。另一個方法是使用帶有 setter 的雙向繫結計算屬性:

<input v-model="message">
複製程式碼
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}
複製程式碼

未完待續,點選檢視更多細節:第 47 題

第 48 題:call 和 apply 的區別是什麼,哪個效能更好一些

解析:

  1. Function.prototype.apply和Function.prototype.call 的作用是一樣的,區別在於傳入引數的不同;
  2. 第一個引數都是,指定函式體內this的指向;
  3. 第二個引數開始不同,apply是傳入帶下標的集合,陣列或者類陣列,apply把它傳給函式作為引數,call從第二個開始傳入的引數是不固定的,都會傳給函式作為引數。
  4. call比apply的效能要好,平常可以多用call, call傳入引數的格式正是內部所需要的格式,參考 call和apply的效能對比

未完待續,點選檢視更多細節:第 48 題

第 49 題:為什麼通常在傳送資料埋點請求的時候使用的是 1x1 畫素的透明 gif 圖片?

解析:

  1. 能夠完成整個 HTTP 請求+響應(儘管不需要響應內容)
  2. 觸發 GET 請求之後不需要獲取和處理資料、伺服器也不需要傳送資料
  3. 跨域友好
  4. 執行過程無阻塞
  5. 相比 XMLHttpRequest 物件傳送 GET 請求,效能上更好
  6. GIF的最低合法體積最小(最小的BMP檔案需要74個位元組,PNG需要67個位元組,而合法的GIF,只需要43個位元組)
  7. 不會阻塞頁面載入,影響使用者的體驗,只要new Image物件就好了;(排除JS/CSS檔案資源方式上報)

未完待續,點選檢視更多細節:第 49 題

第 50 題:實現 (5).add(3).minus(2) 功能。

例: 5 + 3 - 2,結果為 6

解析:

Number.prototype.add = function(n) {
  return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
  return this.valueOf() - n;
};
複製程式碼

未完待續,點選檢視更多細節:第 50 題

第 51 題:Vue 的響應式原理中 Object.defineProperty 有什麼缺陷?

為什麼在 Vue3.0 採用了 Proxy,拋棄了 Object.defineProperty?

解析:

Object.defineProperty本身有一定的監控到陣列下標變化的能力: Object.defineProperty本身是可以監控到陣列下標的變化的,但是在 Vue 中,從效能/體驗的價效比考慮,尤大大就棄用了這個特性。具體我們可以參考 《記一次思否問答的問題思考:Vue為什麼不能檢測陣列變動》這篇文章,文章底部配圖中有尤大大的嚴肅回覆截圖; 下方的討論區也很值得大家下去看一看,有對於 for / forEach / for .. in .. 幾個迴圈方式的討論。

關於 Vue 3.0 的其他資訊我們可以參考 尤大大發布的 Vue 3.0 新特性預覽PPT

直接通過陣列的下標給陣列設定值,不能實時響應。 為了解決這個問題,經過vue內部處理後可以使用以下幾種方法來監聽陣列

push()
pop()
shift()
unshift()
splice()
sort()
reverse()
複製程式碼

由於只針對了以上幾種方法進行了hack處理,所以其他陣列的屬性也是檢測不到的,還是具有一定的侷限性。

Object.defineProperty只能劫持物件的屬性,因此我們需要對每個物件的每個屬性進行遍歷。Vue 2.x裡,是通過 遞迴 + 遍歷 data 物件來實現對資料的監控的,如果屬性值也是物件那麼需要深度遍歷,顯然如果能劫持一個完整的物件是才是更好的選擇。

而要取代它的Proxy有以下兩個優點;

可以劫持整個物件,並返回一個新物件 有13種劫持操作

未完待續,點選檢視更多細節:第 51 題

第 52 題:怎麼讓一個 div 水平垂直居中

解析:

<div class="parent">
  <div class="child"></div>
</div>
複製程式碼
div.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}
複製程式碼
div.parent {
    position: relative; 
}
div.child {
    position: absolute; 
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);  
}
/* 或者 */
div.child {
    width: 50px;
    height: 10px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -25px;
    margin-top: -5px;
}
/* 或 */
div.child {
    width: 50px;
    height: 10px;
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}
複製程式碼
div.parent {
    display: grid;
}
div.child {
    justify-self: center;
    align-self: center;
}
複製程式碼
div.parent {
    font-size: 0;
    text-align: center;
    &::before {
        content: "";
        display: inline-block;
        width: 0;
        height: 100%;
        vertical-align: middle;
    }
}
div.parent{
  display: inline-block;
  vertical-align: middle;
}
複製程式碼

未完待續,點選檢視更多細節:第 52 題

第 53 題:輸出以下程式碼的執行結果並解釋為什麼

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)
複製程式碼

解析:

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

a.x 	// --> undefined
b.x 	// --> {n: 2}
複製程式碼

答案已經寫上面了,這道題的關鍵在於

  • 1、優先順序。.的優先順序高於=,所以先執行a.x,堆記憶體中的{n: 1}就會變成{n: 1, x: undefined},改變之後相應的b.x也變化了,因為指向的是同一個物件。
  • 2、賦值操作是從右到左,所以先執行a = {n: 2}a的引用就被改變了,然後這個返回值又賦值給了a.x需要注意的是這時候a.x是第一步中的{n: 1, x: undefined}那個物件,其實就是b.x,相當於b.x = {n: 2}

img

未完待續,點選檢視更多細節:第 53 題


交流

進階系列文章彙總如下,覺得不錯點個 Star,歡迎 加群 互相學習。

github.com/yygmind/blo…

我是木易楊,公眾號「高階前端進階」作者,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!

【半月刊 4】前端高頻面試題及答案彙總

相關文章