刷《一年半經驗,百度、有贊、阿里面試總結》·手記

在掘金上看到了一位大佬發了一篇很詳細的面試記錄文章-《一年半經驗,百度、有贊、阿里面試總結》,爲了查漏補缺,抽空就詳細作了下。(估計只有我這麼無聊了哈哈哈javascript

有給出的或者有些不完善的答案,也盡力給出/完善了(可能有錯,你們自行辨別)。有些很困難的題目(例如實現Promise),附帶相關連接(懶癌患者福利)。css

總的來講,將這些題目分紅了「Javascript」、「CSS」、「瀏覽器/協議」、「算法」和「Web工程化」5個部分進行回答和代碼實現。html

最後,歡迎來個人博客和我扯犢子:godbmw.com。直接戳本篇原文的地址:刷《一年半經驗,百度、有贊、阿里面試總結》·手記vue

1. Javascript相關

1.1 迴文字符串

題目:實現一個函數,判斷是否是迴文字符串java

原文的思路是將字符串轉化成數組=>反轉數組=>拼接成字符串。這種作法充分利用了js的BIF,但性能有所損耗react

function run(input) {
  if (typeof input !== 'string') return false;
  return input.split('').reverse().join('') === input;
}

其實正常思路也很簡單就能實現,性能更高,可是沒有利用js的特性git

// 迴文字符串
const palindrome = (str) => {
  // 類型判斷
  if(typeof str !== 'string') {
    return false;
  }

  let len = str.length;
  for(let i = 0; i < len / 2; ++i){
    if(str[i] !== str[len - i - 1]){
      return false;
    }
  }
  return true;
}

1.2 實現Storage

題目:實現Storage,使得該對象爲單例,並對localStorage進行封裝設置值setItem(key,value)和getItem(key)es6

題目重點是單例模式,須要注意的是藉助localStorage,不是讓本身手動實現!github

const Storage = () => {}

Storage.prototype.getInstance = (() => {
  let instance = null
  return () => {
    if(!instance){
      instance = new Storage()
    }
    return instance
  }
})()

Storage.prototype.setItem = (key, value) => {
  return localStorage.setItem(key, value)
}

Storage.prototype.getItem = (key) => {
  return localStorage.getItem(key)
}

1.3 JS事件流

題目:說說事件流吧web

事件流分爲冒泡和捕獲。

事件冒泡:子元素的觸發事件會一直向父節點傳遞,一直到根結點中止。此過程當中,能夠在每一個節點捕捉到相關事件。能夠經過stopPropagation方法終止冒泡。

事件捕獲:和「事件冒泡」相反,從根節點開始執行,一直向子節點傳遞,直到目標節點。印象中只有少數瀏覽器的老舊版本纔是這種事件流,能夠忽略。

1.4 實現函數繼承

題目:如今有一個函數A和函數B,請你實現B繼承A。而且說明他們優缺點。

方法一:綁定構造函數

優勢:能夠實現多繼承

缺點:不能繼承父類原型方法/屬性

function Animal(){
  this.species = "動物";
}

function Cat(){
  Animal.apply(this, arguments); // 父對象的構造函數綁定到子節點上
}

var cat = new Cat()
console.log(cat.species) // 輸出:動物

方法二:原型鏈繼承

優勢:可以繼承父類原型和實例方法/屬性,而且能夠捕獲父類的原型鏈改動

缺點:沒法實現多繼承,會浪費一些內存(Cat.prototype.constructor = Cat)。除此以外,須要注意應該將Cat.prototype.constructor從新指向自己。

js中交換原型鏈,均須要修復prototype.constructor指向問題。

function Animal(){
  this.species = "動物";
}
Animal.prototype.func = function(){
  console.log("heel")
}

function Cat(){}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

var cat = new Cat()
console.log(cat.func, cat.species)

方法3:結合上面2種方法

function Animal(){
  this.species = "動物";
}
Animal.prototype.func = function(){
  console.log("heel")
}

function Cat(){
  Animal.apply(this, arguments)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat;

var cat = new Cat()
console.log(cat.func, cat.species)

1.5 ES5對象 vs ES6對象

題目:es6 class 的new實例和es5的new實例有什麼區別?

ES6中(和ES5相比),classnew實例有如下特色:

  • class的構造參數必須是new來調用,不能夠將其做爲普通函數執行
  • es6class不存在變量提高
  • 最重要的是:es6內部方法不能夠枚舉。es5的prototype上的方法能夠枚舉。

爲此我作了如下測試代碼進行驗證:

console.log(ES5Class()) // es5:能夠直接做爲函數運行
// console.log(new ES6Class()) // 會報錯:不存在變量提高

function ES5Class(){
  console.log("hello")
}

ES5Class.prototype.func = function(){ console.log("Hello world") }

class ES6Class{
  constructor(){}
  func(){
    console.log("Hello world")
  }
}

let es5 = new ES5Class()
let es6 = new ES6Class()

console.log("ES5 :")
for(let _ in es5){
  console.log(_)
}

// es6:不可枚舉
console.log("ES6 :")
for(let _ in es6){
  console.log(_)
}

這篇《JavaScript建立對象—從es5到es6》對這個問題的深刻解釋很好,推薦觀看!

1.6 實現MVVM

題目:請簡單實現雙向數據綁定mvvm

vuejs是利用Object.defineProperty來實現的MVVM,採用的是訂閱發佈模式。每一個data中都有set和get屬性,這種點對點的效率,比Angular實現MVVM的方式的效率更高。

<body>
  <input type="text">
  <script>
    const input = document.querySelector('input')
    const obj = {}

    Object.defineProperty(obj, 'data', {
      enumerable: false,  // 不可枚舉
      configurable: false, // 不可刪除
      set(value){
        input.value = value
        _value = value
        // console.log(input.value)
      },
      get(){
        return _value
      }
    }) 
    obj.data = '123'
    input.onchange = e => {
      obj.data = e.target.value
    }
  </script>
</body>

1.7 實現Promise

這是一位大佬實現的Promise版本:過了Promie/A+標準的測試!!!網上能搜到的基本都是從這篇文章變形而來或者直接照搬!!!原文地址,直接戳:剖析Promise內部結構,一步一步實現一個完整的、能經過全部Test case的Promise類

下面附上一種近乎完美的實現:可能沒法和其餘Promise庫的實現無縫對接。可是,上面的原文實現了所有的,歡迎Mark!

function MyPromise(executor){
  var that = this
  this.status = 'pending' // 當前狀態
  this.data = undefined
  this.onResolvedCallback = [] // Promise resolve時的回調函數集,由於在Promise結束以前有可能有多個回調添加到它上面
  this.onRejectedCallback = [] // Promise reject時的回調函數集,由於在Promise結束以前有可能有多個回調添加到它上面

  // 更改狀態 => 綁定數據 => 執行回調函數集
  function resolve(value){
    if(that.status === 'pending'){
      that.status = 'resolved'
      that.data = value
      for(var i = 0; i < that.onResolvedCallback.length; ++i){
        that.onResolvedCallback[i](value)
      }
    }
  }

  function reject(reason){
    if(that.status === 'pending'){
      that.status = 'rejected'
      that.data = reason
      for(var i = 0; i < that.onResolvedCallback.length; ++i){
        that.onRejectedCallback[i](reason)
      }
    }
  }

  try{ 
    executor(resolve, reject) // resolve, reject兩個函數能夠在外部傳入的函數(executor)中調用
  } catch(e) { // 考慮到執行過程可能有錯
    reject(e)
  }
}

// 標準是沒有catch方法的,實現了then,就實現了catch
// then/catch 均要返回一個新的Promise實例

MyPromise.prototype.then = function(onResolved, onRejected){
  var that = this
  var promise2

  // 值穿透
  onResolved = typeof onResolved === 'function' ? onResolved : function(v){ return v }
  onRejected = typeof onRejected === 'function' ? onRejected : function(r){ return r }

  if(that.status === 'resolved'){
    return promise2 = new MyPromise(function(resolve, reject){
      try{
        var x = onResolved(that.data)
        if(x instanceof MyPromise){ // 若是onResolved的返回值是一個Promise對象,直接取它的結果作爲promise2的結果
          x.then(resolve, reject)
        }
        resolve(x) // 不然,以它的返回值作爲promise2的結果 
      } catch(e) {
        reject(e) // 若是出錯,以捕獲到的錯誤作爲promise2的結果
      }
    })
  }

  if(that.status === 'rejected'){
    return promise2 = new MyPromise(function(resolve, reject){
      try{
        var x = onRejected(that.data)
        if(x instanceof MyPromise){
          x.then(resolve, reject)
        }
      } catch(e) {
        reject(e)
      }
    })
  }

  if(that.status === 'pending'){
    return promise2 = new MyPromise(function(resolve, reject){
      self.onResolvedCallback.push(function(reason){
        try{
          var x = onResolved(that.data)
          if(x instanceof MyPromise){
            x.then(resolve, reject)
          }
        } catch(e) {
          reject(e)
        }
      })

      self.onRejectedCallback.push(function(value){
        try{
          var x = onRejected(that.data)
          if(x instanceof MyPromise){
            x.then(resolve, reject)
          }
        } catch(e) {
          reject(e)
        }
      })
    })
  }
}

MyPromise.prototype.catch = function(onRejected){
  return this.then(null, onRejected)
}

// 如下是簡單的測試樣例:
new MyPromise(resolve => resolve(8)).then(value => {
  console.log(value)
})

1.8 Event Loop

題目:說一下JS的EventLoop

其實阮一峯老師這篇《JavaScript 運行機制詳解:再談Event Loop》已經講的很清晰了(手動贊)!

這裏簡單總結下:

  1. JS是單線程的,其上面的全部任務都是在兩個地方執行:執行棧和任務隊列。前者是存放同步任務;後者是異步任務有結果後,就在其中放入一個事件。
  2. 當執行棧的任務都執行完了(棧空),js會讀取任務隊列,並將能夠執行的任務從任務隊列丟到執行棧中執行。
  3. 這個過程是循環進行,因此稱做Loop

2. CSS相關

2.1 水平垂直居中

題目: 兩種以上方式實現已知或者未知寬度的垂直水平居中

第一種方法就是利用CSS3translate進行偏移定位,注意:兩個參數的百分比都是針對元素自己計算的。

.wrap {
  position: relative;
  width: 100vw;
  height: 100vh;
}

.box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

第二種方法是利用CSS3flex佈局,父元素diplay屬性設置爲flex,而且定義元素在兩條軸線的佈局方式均爲center

.wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100vw;
  height: 100vh;
}

.wrap .box {
  width: 100px;
  height: 100px;
}

第三種方法是利用margin負值來進行元素偏移,優勢是瀏覽器兼容好,缺點是不夠靈活(要自行計算margin的值):

.wrap {
  position: relative;
  width: 100vw;
  height: 100vh;
}

.box {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100px;
  height: 100px;
  margin: -50px 0 0 -50px;
}

2.2 「點擊」改變樣式

題目:實現效果,點擊容器內的圖標,圖標邊框變成border 1px solid red,點擊空白處重置。

利用event.target能夠判斷是不是指定元素自己(判斷「空白處」),除此以外,注意禁止冒泡(題目指明瞭「容器內」)。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
  #app {
    min-width: 100vw;
    min-height: 100vh;
  }
  #app .icon{
    display: inline-block;
    cursor: pointer;
  }
  </style>
</head>
<body>
  <div id="app">
    <span class="icon">123456</span>
  </div>
  <script>
  const app = document.querySelector("#app")
  const icon = document.querySelector(".icon")

  app.addEventListener("click", e => {
    if(e.target === icon){
      return;
    }
    // 非空白處纔去除 border 
    icon.style.border = "none";
  })

  icon.addEventListener("click", e => {
    // 禁止冒泡
    e.stopPropagation()
    // 更改樣式
    icon.style.border = "1px solid red";
  })
  </script>
</body>
</html>

3. 瀏覽器/協議相關

3.1 緩存機制

題目:說一下瀏覽器的緩存機制。

瀏覽器緩存分爲強緩存和協商緩存。緩存的做用是提升客戶端速度、節省網絡流量、下降服務器壓力

強緩存:瀏覽器請求資源,若是header中的Cache-ControlExpires沒有過時,直接從緩存(本地)讀取資源,不須要再向服務器請求資源。

協商緩存:瀏覽器請求的資源若是是過時的,那麼會向服務器發送請求,header中帶有Etag字段。服務器再進行判斷,若是ETag匹配,則返回給客戶端300系列狀態碼,客戶端繼續使用本地緩存;不然,客戶端會從新獲取數據資源。

關於過程當中詳細的字段,能夠參考這篇《http協商緩存VS強緩存

3.2 從URL到頁面生成

題目:輸入URL到看到頁面發生的全過程,越詳細越好

  1. DNS解析
  2. 創建TCP鏈接(3次握手)
  3. 發送HTTP請求,從服務器下載相關內容
  4. 瀏覽器構建DOM樹和CSS樹,而後生成渲染樹。這個一個漸進式過程,引擎會力求最快將內容呈現給用戶。
  5. 在第四步的過程當中,<script>的位置和加載方式會影響響應速度。
  6. 搞定了,關閉TCP鏈接(4次握手)

3.3 TCP握手

題目:解釋TCP創建的時候的3次握手和關閉時候的4次握手

看這題的時候,我也是忽然懵(手動捂臉)。推薦翻一下計算機網絡的相關書籍,對於FINACK等字段的講解很贊!

3.4 CSS和JS位置

題目:CSS和JS的位置會影響頁面效率,爲何?

先說CSS。CSS的位置不會影響加載速度,可是CSS通常放在<head>標籤中。前面有說DOM樹和CSS樹共同生成渲染樹,CSS位置太靠後的話,在CSS加載以前,可能會出現閃屏、樣式混亂、白屏等狀況。

再說JS。JS是阻塞加載,默認的<script>標籤會加載而且當即執行腳本,若是腳本很複雜或者網絡很差,會出現好久的白屏。因此,JS標籤通常放到<body>標籤最後。

如今,也能夠爲<script>標籤設置async或者defer屬性。前者是js腳本的加載和執行將與後續文檔的加載和渲染同步執行。後者是js腳本的加載將與後續文檔的加載和渲染同步執行,當全部元素解析完,再執行js腳本。

4. 算法相關

4.1 數組全排列

題目:如今有一個數組[1,2,3,4],請實現算法,獲得這個數組的全排列的數組,如[2,1,3,4],[2,1,4,3]。。。。你這個算法的時間複雜度是多少

實現思路:從「開始元素」起,每一個元素都和開始元素進行交換;不斷縮小範圍,最後輸出這種排列。暴力法的時間複雜度是 \(O(N_N)\),遞歸實現的時間複雜度是 \(O(N!)\)

如何去重?去重的全排列就是從第一個數字起每一個數分別與它後面非重複出現的數字交換。對於有重複元素的數組,例如:[1, 2, 2],應該剔除重複的狀況。每次只須要檢查arr[start, i)中是否是有和arr[i]相同的元素,有的話,說明以前已經輸出過了,不須要考慮。

代碼實現:

const swap = (arr, i, j) => {
  let tmp = arr[i]
  arr[i] = arr[j]
  arr[j] = tmp
}

const permutation = arr => {
  const _permutation = (arr, start) => {
    if(start === arr.length){
      console.log(arr)
      return
    }
    for(let i = start; i < arr.length; ++i){
      // 全排列:去重操做
      if(arr.slice(start, i).indexOf(arr[i]) !== -1){
        continue
      }
      swap(arr, i, start) // 和開始元素進行交換
      _permutation(arr, start + 1)
      swap(arr, i, start) // 恢復數組
    }
    return 
  }
  return _permutation(arr, 0)
}

permutation([1, 2, 2])
console.log("**********")
permutation([1, 2, 3, 4])

4.2 揹包問題

題目:我如今有一個揹包,容量爲m,而後有n個貨物,重量分別爲w1,w2,w3...wn,每一個貨物的價值是v1,v2,v3...vn,w和v沒有任何關係,請求揹包能裝下的最大價值。

這個還在學習中,揹包問題博大精深。。。

4.3 圖的連通份量

題目:我如今有一個canvas,上面隨機布着一些黑塊,請實現方法,計算canvas上有多少個黑塊。

這一題能夠轉化成圖的聯通份量問題。經過getImageData得到像素數組,從頭至尾遍歷一遍,就能夠判斷每一個像素是不是黑色。同時,準備一個width * height大小的二維數組,這個數組的每一個元素是1/0。若是是黑色,二維數組對應元素就置1;不然置0。

而後問題就被轉換成了圖的連通份量問題。能夠經過深度優先遍歷或者並查集來實現。以前我用C++實現了,這裏再也不冗贅:

5. Web工程化

5.1 Dialog組件思路

題目:如今要你完成一個Dialog組件,說說你設計的思路?它應該有什麼功能?

  • 能夠指定寬度、高度和位置
  • 須要一個遮蓋層,遮住底層內容
  • 由頭部、尾部和正文構成
  • 須要監聽事件和自定義事件,非單向數據流:例如點擊組件右上角,修改父組件的visible屬性,關閉組件。

關於工程化組件封裝,能夠去試試ElementUI。這個是ElementUI的Dialog組件:Element-Dialog

5.2 React的Diff算法和虛擬DOM

題目: react 的虛擬dom是怎麼實現的

原答案寫的挺好滴,這裏直接貼了。

首先說說爲何要使用Virturl DOM,由於操做真實DOM的耗費的性能代價過高,因此react內部使用js實現了一套dom結構。
在每次操做在和真實dom以前,使用實現好的diff算法,對虛擬dom進行比較,遞歸找出有變化的dom節點,而後對其進行更新操做。
爲了實現虛擬DOM,咱們須要把每一種節點類型抽象成對象,每一種節點類型有本身的屬性,也就是prop,每次進行diff的時候,react會先比較該節點類型:
假如節點類型不同,那麼react會直接刪除該節點,而後直接建立新的節點插入到其中;
假如節點類型同樣,那麼會比較prop是否有更新,假若有prop不同,那麼react會斷定該節點有更新,那麼重渲染該節點,而後在對其子節點進行比較,一層一層往下,直到沒有子節點。

參考連接:React源碼之Diff算法

最後,歡迎來個人博客和我扯犢子:godbmw.com。直接戳本篇原文的地址:刷《一年半經驗,百度、有贊、阿里面試總結》·手記

相關文章
相關標籤/搜索