面試圖譜:前端基礎技術知識講解

yck發表於2018-07-23
面試圖譜:前端基礎技術知識講解

前言

當你老了,回顧一生,就會發覺:什麼時候出國讀書,什麼時候決定做第一份職業,何時選定物件而戀愛,什麼時候結婚,其實都是命運的鉅變。只是當時站在三岔路口,眼見風雲千檣,你做出選擇的那一日,在日記上,相當沉悶和平凡,當時還以為是生命中普通的一天。

一個改變面試的專案 -- 面試圖譜

金九銀十的秋招季近在眼前,想必大家也都心癢難耐,準備挑戰更好的工作機會。那麼,面試肯定是最大的挑戰。

對於面試來說,平時的積累肯定是必須的,但是在面試前的準備也是至關重要的。

在幾月前我個人組建了一個小團隊,花了將近半年的時間尋找大廠的面試題,篩選出了近百個知識點然後成文,並全部翻譯為英文。今天,終於開源出了第一個版本,目前總字數已高達 10 餘萬字。團隊的每個成員都是來自一線的工程師,其中還有一個來自谷歌的大佬。

可以這樣說,我們有自信,把這個專案打造成關於面試相關中的 NO 1. 因為所有內容都沒有照搬任何的書籍,每一個知識點都有詳細的查閱過資料才成文。後期更會加入更多的內容,儘可能地覆蓋大部分知識點。

我們認為,一味的背面試題是沒多大作用的。只有熟悉了各個知識點並融會貫通,才能在面試中披荊斬棘。本圖譜目前包含了近百個高頻知識點,無論是面試前的準備還是平時學習中的查漏補缺,我們相信肯定能幫助到大家。目前內容包含了 JS、網路、瀏覽器相關、效能優化、安全、框架、Git、資料結構、演算法等內容,無論是基礎還是進階,亦或是原始碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的準備面試。

該倉庫內容會持續更新,後期將會包含更多的內容,比如:系統設計、區塊鏈、運維、後端等等,當然這些不是我的強項,我會邀請這方面有不錯經驗的朋友來書寫內容。

大綱

面試圖譜:前端基礎技術知識講解

求職

最近本人在尋找工作機會,如果有杭州的不錯崗位的話,歡迎聯絡我 zx597813039@gmail.com

部分內容預覽

MVVM

MVVM 由以下三個內容組成

  • View:介面
  • Model:資料模型
  • ViewModel:作為橋樑負責溝通 View 和 Model

在 JQuery 時期,如果需要重新整理 UI 時,需要先取到對應的 DOM 再更新 UI,這樣資料和業務的邏輯就和頁面有強耦合。

在 MVVM 中,UI 是通過資料驅動的,資料一旦改變就會相應的重新整理對應的 UI,UI 如果改變,也會改變對應的資料。這種方式就可以在業務處理中只關心資料的流轉,而無需直接和頁面打交道。ViewModel 只關心資料和業務的處理,不關心 View 如何處理資料,在這種情況下,View 和 Model 都可以獨立出來,任何一方改變了也不一定需要改變另一方,並且可以將一些可複用的邏輯放在一個 ViewModel 中,讓多個 View 複用這個 ViewModel。

在 MVVM 中,最核心的也就是資料雙向繫結,例如 Angluar 的髒資料檢測,Vue 中的資料劫持。

髒資料檢測

當觸發了指定事件後會進入髒資料檢測,這時會呼叫 $digest 迴圈遍歷所有的資料觀察者,判斷當前值是否和先前的值有區別,如果檢測到變化的話,會呼叫 $watch 函式,然後再次呼叫 $digest 迴圈直到發現沒有變化。迴圈至少為二次 ,至多為十次。

髒資料檢測雖然存在低效的問題,但是不關心資料是通過什麼方式改變的,都可以完成任務,但是這在 Vue 中的雙向繫結是存在問題的。並且髒資料檢測可以實現批量檢測出更新的值,再去統一更新 UI,大大減少了操作 DOM 的次數。所以低效也是相對的,這就仁者見仁智者見智了。

資料劫持

Vue 內部使用了 Obeject.defineProperty() 來實現雙向繫結,通過這個函式可以監聽到 setget 的事件。

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判斷型別
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

function defineReactive(obj, key, val) {
  // 遞迴子屬性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}
複製程式碼

以上程式碼簡單的實現瞭如何監聽資料的 setget 的事件,但是僅僅如此是不夠的,還需要在適當的時候給屬性新增發布訂閱

<div>
    {{name}}
</div>
複製程式碼

在解析如上模板程式碼時,遇到 {{name}} 就會給屬性 name 新增發布訂閱。

// 通過 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 例項
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全域性屬性,通過該屬性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 將 Dep.target 指向自己
    // 然後觸發屬性的 getter 新增監聽
    // 最後將 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 獲得新值
    this.value = this.obj[this.key]
    // 呼叫 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模擬解析到 `{{name}}` 觸發的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 
複製程式碼

接下來,對 defineReactive 函式進行改造

function defineReactive(obj, key, val) {
  // 遞迴子屬性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 將 Watcher 新增到訂閱
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 執行 watcher 的 update 方法
      dp.notify()
    }
  })
}
複製程式碼

以上實現了一個簡易的雙向繫結,核心思路就是手動觸發一次屬性的 getter 來實現釋出訂閱的新增。

Proxy 與 Obeject.defineProperty 對比

Obeject.defineProperty 雖然已經能夠實現雙向繫結了,但是他還是有缺陷的。

  1. 只能對屬性進行資料劫持,所以需要深度遍歷整個物件
  2. 對於陣列不能監聽到資料的變化

雖然 Vue 中確實能檢測到陣列資料的變化,但是其實是使用了 hack 的辦法,並且也是有缺陷的。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 以下幾個函式
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 獲得原生函式
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 呼叫原生函式
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 觸發更新
    ob.dep.notify()
    return result
  })
})
複製程式碼

反觀 Proxy 就沒以上的問題,原生支援監聽陣列變化,並且可以直接對整個物件進行攔截,所以 Vue 也將在下個大版本中使用 Proxy 替換 Obeject.defineProperty

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
複製程式碼

路由原理

前端路由實現起來其實很簡單,本質就是監聽 URL 的變化,然後匹配路由規則,顯示相應的頁面,並且無須重新整理。目前單頁面使用的路由就只有兩種實現方式

  • hash 模式
  • history 模式

www.test.com/#/ 就是 Hash URL,當 # 後面的雜湊值發生變化時,不會向伺服器請求資料,可以通過 hashchange 事件來監聽到 URL 的變化,從而進行跳轉頁面。

面試圖譜:前端基礎技術知識講解

History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美觀

面試圖譜:前端基礎技術知識講解

內建型別

JS 中分為七種內建型別,七種內建型別又分為兩大型別:基本型別和物件(Object)。

基本型別有六種: nullundefinedbooleannumberstringsymbol

其中 JS 的數字型別是浮點型別的,沒有整型。並且浮點型別基於 IEEE 754標準實現,在使用中會遇到某些 BugNaN 也屬於 number 型別,並且 NaN 不等於自身。

對於基本型別來說,如果使用字面量的方式,那麼這個變數只是個字面量,只有在必要的時候才會轉換為對應的型別

let a = 111 // 這只是字面量,不是 number 型別
a.toString() // 使用時候才會轉換為物件型別
複製程式碼

物件(Object)是引用型別,在使用過程中會遇到淺拷貝和深拷貝的問題。

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF
複製程式碼

Typeof

typeof 對於基本型別,除了 null 都可以顯示正確的型別

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 沒有宣告,但是還會顯示 undefined
複製程式碼

typeof 對於物件,除了函式都會顯示 object

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
複製程式碼

對於 null 來說,雖然它是基本型別,但是會顯示 object,這是一個存在很久了的 Bug

typeof null // 'object'
複製程式碼

PS:為什麼會出現這種情況呢?因為在 JS 的最初版本中,使用的是 32 位系統,為了效能考慮使用低位儲存了變數的型別資訊,000 開頭代表是物件,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部型別判斷程式碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

如果我們想獲得一個變數的正確型別,可以通過 Object.prototype.toString.call(xx)。這樣我們就可以獲得類似 [Object Type] 的字串。

let a
// 我們也可以這樣判斷 undefined
a === undefined
// 但是 undefined 不是保留字,能夠在低版本瀏覽器被賦值
let undefined = 1
// 這樣判斷就會出錯
// 所以可以用下面的方式來判斷,並且程式碼量更少
// 因為 void 後面隨便跟上一個組成表示式
// 返回就是 undefined
a === void 0
複製程式碼

型別轉換

轉Boolean

在條件判斷時,除了 undefinednullfalseNaN''0-0,其他所有值都轉為 true,包括所有物件。

物件轉基本型別

物件在轉換基本型別時,首先會呼叫 valueOf 然後呼叫 toString。並且這兩個方法你是可以重寫的。

let a = {
    valueOf() {
    	return 0
    }
}
複製程式碼

四則運算子

只有當加法運算時,其中一方是字串型別,就會把另一個也轉為字串型別。其他運算只要其中一方是數字,那麼另一方就轉為數字。並且加法運算會觸發三種型別轉換:將值轉換為原始值,轉換為數字,轉換為字串。

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
複製程式碼

對於加號需要注意這個表示式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
// 因為 + 'b' -> NaN
// 你也許在一些程式碼中看到過 + '1' -> 1
複製程式碼

== 操作符

面試圖譜:前端基礎技術知識講解

上圖中的 toPrimitive 就是物件轉基本型別。

一般推薦使用 === 判斷兩個值,但是你如果想知道一個值是不是 null ,你可以通過 xx == null 來比較。

這裡來解析一道題目 [] == ![] // -> true ,下面是這個表示式為何為 true 的步驟

// [] 轉成 true,然後取反變成 false
[] == false
// 根據第 8 條得出
[] == ToNumber(false)
[] == 0
// 根據第 10 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據第 6 條得出
0 == 0 // -> true
複製程式碼

比較運算子

  1. 如果是物件,就通過 toPrimitive 轉換物件
  2. 如果是字串,就通過 unicode 字元索引來比較

公眾號

面試圖譜:前端基礎技術知識講解

Todo

  • 完成 CSS 內容
  • 完成 Webapck 內容
  • 完成小程式相關內容
  • 完善關於框架的內容

以上內容預計將於 9 月份更新完畢,歡迎你一起參與本圖譜的建設。

面試圖譜 專案地址,如果你覺得專案還不錯,你可以點個小星星支援我們一下,你的支援是我們更新的源動力。

相關文章