17K star 倉庫,解決 90% 的大廠基礎面試題

前言

筆者開源的前端進階之道已有三年之久,至今也有 17k star,承蒙各位讀者垂愛。在當下部份內容已經略微過期,所以決定提筆翻新內容。html

翻新後的內容會所有集合在「幹爆前端」中,有興趣的讀者能夠前往查看。前端

閱讀前重要提示:c++

本文非百科全書,只專爲面試複習準備、查漏補缺、深刻某知識點的引子、瞭解相關面試題等準備。git

筆者一直都是崇尚學會面試題底下涉及到的知識點,而不是刷一大堆面試題,結果變了個題型就不會的那種。因此本文和別的面經不同,旨在提煉面試題底下的經常使用知識點,而不是甩一大堆面試題給各位看官。github

你們也能夠在筆者的 網站上閱讀,體驗更佳!

數據類型

JS 數據類型分爲兩大類,九個數據類型:面試

  1. 原始類型
  2. 對象類型

其中原始類型又分爲七種類型,分別爲:算法

  • boolean
  • number
  • string
  • undefined
  • null
  • symbol
  • bigint

對象類型分爲兩種,分別爲:windows

  • Object
  • Function

其中 Object 中又包含了不少子類型,好比 ArrayRegExpMathMapSet 等等,也就不一一列出了。數組

原始類型存儲在棧上,對象類型存儲在堆上,可是它的引用地址仍是存在棧上。promise

注意:以上結論前半句是不許確的,更準確的內容我會在閉包章節裏說明。

常見考點

  • JS 類型有哪些?
  • 大數相加、相乘算法題,能夠直接使用 bigint,固然再加上字符串的處理會更好。
  • NaN 如何判斷

另外還有一類常見的題目是對於對象的修改,好比說往函數裏傳一個對象進去,函數內部修改參數。

function test(person) {
  person.age = 26
  person = {}

  return person
}
const p1 = {
  age: 25
}

這類題目咱們只須要牢記如下幾點:

  1. 對象存儲的是引用地址,傳來傳去、賦值給別人那都是在傳遞值(存在棧上的那個內容),別人一旦修改對象裏的屬性,你們都被修改了。
  2. 可是一旦對象被從新賦值了,只要不是原對象被從新賦值,那麼就永遠不會修改原對象。

類型判斷

類型判斷有好幾種方式。

typeof

原始類型中除了 null,其它類型均可以經過 typeof 來判斷。

typeof null 的值爲 object,這是由於一個久遠的 Bug,沒有細究的必要,瞭解便可。若是想具體判斷 null 類型的話直接 xxx === null 便可。

對於對象類型來講,typeof 只能具體判斷函數的類型爲 function,其它均爲 object

instanceof

instanceof 內部經過原型鏈的方式來判斷是否爲構建函數的實例,經常使用於判斷具體的對象類型。

[] instanceof Array

都說 instanceof 只能判斷對象類型,其實這個說法是不許確的,咱們是能夠經過 hake 的方式得以實現,雖然不會有人這樣去玩吧。

class CheckIsNumber {
  static [Symbol.hasInstance](number) {
    return typeof number === 'number'
  }
}

// true
1 instanceof CheckIsNumber

另外其實咱們還能夠直接經過構建函數來判斷類型:

// true
[].constructor === Array

Object.prototype.toString

前幾種方式或多或少都存在一些缺陷,Object.prototype.toString 綜合來看是最佳選擇,能判斷的類型最完整。

上圖是一部分類型判斷,更多的就不列舉了,[object XXX] 中的 XXX 就是判斷出來的類型。

isXXX API

同時還存在一些判斷特定類型的 API,選了兩個常見的:

常見考點

  • JS 類型如何判斷,有哪幾種方式可用
  • instanceof 原理
  • 手寫 instanceof

類型轉換

類型轉換分爲兩種狀況,分別爲強制轉換及隱式轉換。

強制轉換

強制轉換就是轉成特定的類型:

Number(false) // -> 0
Number('1') // -> 1
Number('zb') // -> NaN
(1).toString() // '1'

這部分是平常經常使用的內容,就不具體展開說了,主要記住強制轉數字和布爾值的規則就行。

轉布爾值規則:

  • undefined、null、false、NaN、''、0、-0 都轉爲 false
  • 其餘全部值都轉爲 true,包括全部對象。

轉數字規則:

  • true 爲 1,false 爲 0
  • null 爲 0,undefinedNaNsymbol 報錯
  • 字符串看內容,若是是數字或者進制值就正常轉,不然就 NaN
  • 對象的規則隱式轉換再講

隱式轉換

隱式轉換規則是最煩的,其實筆者也記不住那麼多內容。何況根據筆者目前收集到的最新面試題來講,這部分考題基本絕跡了,固然講仍是講一下吧。

對象轉基本類型:

  • 調用 Symbol.toPrimitive,轉成功就結束
  • 調用 valueOf,轉成功就結束
  • 調用 toString,轉成功就結束
  • 報錯

四則運算符:

  • 只有當加法運算時,其中一方是字符串類型,就會把另外一個也轉爲字符串類型
  • 其餘運算只要其中一方是數字,那麼另外一方就轉爲數字

== 操做符

常見考點

若是這部分規則記不住也不礙事,確實有點繁瑣,並且考的也愈來愈少了,拿一道之前常考的題目看看吧:

[] == ![] // -> ?

this

this 是不少人會混淆的概念,可是其實他一點都不難,不要被那些長篇大論的文章嚇住了(我其實也不知道爲何他們能寫那麼多字),你只須要記住幾個規則就能夠了。

普通函數

function foo() {
    console.log(this.a)
}
var a = 1
foo()

var obj = {
    a: 2,
    foo: foo
}
obj.foo()

// 以上狀況就是看函數是被誰調用,那麼 `this` 就是誰,沒有被對象調用,`this` 就是 `window`

// 如下狀況是優先級最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 還有種就是利用 call,apply,bind 改變 this,這個優先級僅次於 new

箭頭函數

由於箭頭函數沒有 this,因此一切妄圖改變箭頭函數 this 指向都是無效的。

箭頭函數的 this 只取決於定義時的環境。好比以下代碼中的 fn 箭頭函數是在 windows 環境下定義的,不管如何調用,this 都指向 window

var a = 1
const fn = () => {
  console.log(this.a)
}
const obj = {
  fn,
  a: 2
}
obj.fn()

常見考點

這裏通常都是考 this 的指向問題,牢記上述的幾個規則就夠用了,好比下面這道題:

const a = {
  b: 2,
  foo: function () { console.log(this.b) }
}

function b(foo) {
  // 輸出什麼?
  foo()
}

b(a.foo)

閉包

首先閉包正確的定義是:假如一個函數能訪問外部的變量,那麼這個函數它就是一個閉包,而不是必定要返回一個函數。這個定義很重要,下面的內容須要用到。

let a = 1
// fn 是閉包
function fn() {
  console.log(a);
}

function fn1() {
  let a = 1
  // 這裏也是閉包
  return () => {
    console.log(a);
  }
}
const fn2 = fn1()
fn2()

你們都知道閉包其中一個做用是訪問私有變量,就好比上述代碼中的 fn2 訪問到了 fn1 函數中的變量 a。可是此時 fn1 早已銷燬,咱們是如何訪問到變量 a 的呢?不是都說原始類型是存放在棧上的麼,爲何此時卻沒有被銷燬掉?

接下來筆者會根據瀏覽器的表現來從新理解關於原始類型存放位置的說法。

先來講下數據存放的正確規則是:局部、佔用空間肯定的數據,通常會存放在棧中,不然就在堆中(也有例外)。 那麼接下來咱們能夠經過 Chrome 來幫助咱們驗證這個說法說法。

上圖中畫紅框的位置咱們能看到一個內部的對象 [[Scopes]],其中存放着變量 a,該對象是被存放在堆上的,其中包含了閉包、全局對象等等內容,所以咱們能經過閉包訪問到本該銷燬的變量。

另外最開始咱們對於閉包的定位是:假如一個函數能訪問外部的變量,那麼這個函數它就是一個閉包,所以接下來咱們看看在全局下的表現是怎麼樣的。

let a = 1
var b = 2
// fn 是閉包
function fn() {
  console.log(a, b);
}

從上圖咱們能發現全局下聲明的變量,若是是 var 的話就直接被掛到 globe 上,若是是其餘關鍵字聲明的話就被掛到 Script 上。雖然這些內容一樣仍是存在 [[Scopes]],可是全局變量應該是存放在靜態區域的,由於全局變量無需進行垃圾回收,等須要回收的時候整個應用都沒了。

只有在下圖的場景中,原始類型纔多是被存儲在棧上。

這裏爲何要說可能,是由於 JS 是門動態類型語言,一個變量聲明時能夠是原始類型,立刻又能夠賦值爲對象類型,而後又回到原始類型。這樣頻繁的在堆棧上切換存儲位置,內部引擎是否是也會有什麼優化手段,或者乾脆所有都丟堆上?只有 const 聲明的原始類型才必定存在棧上?固然這只是筆者的一個推測,暫時沒有深究,讀者能夠忽略這段瞎想。

所以筆者對於原始類型存儲位置的理解爲:局部變量纔是被存儲在棧上,全局變量存在靜態區域上,其它都存儲在堆上。

固然這個理解是創建的 Chrome 的表現之上的,在不一樣的瀏覽器上由於引擎的不一樣,可能存儲的方式仍是有所變化的。

常見考點

閉包能考的不少,概念和筆試題都會考。

概念題就是考考閉包是什麼了。

筆試題的話基本都會結合上異步,好比最多見的:

for (var i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
}

這道題會問輸出什麼,有哪幾種方式能夠獲得想要的答案?

new

new 操做符能夠幫助咱們構建出一個實例,而且綁定上 this,內部執行步驟可大概分爲如下幾步:

  1. 新生成了一個對象
  2. 對象鏈接到構造函數原型上,並綁定 this
  3. 執行構造函數代碼
  4. 返回新對象

在第四步返回新對象這邊有一個狀況會例外:

function Test(name) {
  this.name = name
  console.log(this) // Test { name: 'yck' }
  return { age: 26 }
}
const t = new Test('yck')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

當在構造函數中返回一個對象時,內部建立出來的新對象就被咱們返回的對象所覆蓋,因此通常來講構建函數就別返回對象了(返回原始類型不影響)。

常見考點

  • new 作了那些事?
  • new 返回不一樣的類型時會有什麼表現?
  • 手寫 new 的實現過程

做用域

做用域能夠理解爲變量的可訪問性,總共分爲三種類型,分別爲:

  • 全局做用域
  • 函數做用域
  • 塊級做用域,ES6 中的 letconst 就能夠產生該做用域

其實看完前面的閉包、this 這部份內部的話,應該基本能瞭解做用域的一些應用。

一旦咱們將這些做用域嵌套起來,就變成了另一個重要的知識點「做用域鏈」,也就是 JS 究竟是如何訪問須要的變量或者函數的。

首先做用域鏈是在定義時就被肯定下來的,和箭頭函數裏的 this 同樣,後續不會改變,JS 會一層層往上尋找須要的內容。

其實做用域鏈這個東西咱們在閉包小結中已經看到過它的實體了:[[Scopes]]

圖中的 [[Scopes]] 是個數組,做用域的一層層往上尋找就等同於遍歷 [[Scopes]]

常見考點

  • 什麼是做用域
  • 什麼是做用域鏈

原型

原型在面試裏只須要幾句話、一張圖的概念就夠用了,沒人會讓你長篇大論講上一堆內容的,問原型更多的是爲了引出繼承這個話題。

根據上圖,原型總結下來的概念爲:

  • 全部對象都有一個屬性 __proto__ 指向一個對象,也就是原型
  • 每一個對象的原型均可以經過 constructor 找到構造函數,構造函數也能夠經過 prototype 找到原型
  • 全部函數均可以經過 __proto__ 找到 Function 對象
  • 全部對象均可以經過 __proto__ 找到 Object 對象
  • 對象之間經過 __proto__ 鏈接起來,這樣稱之爲原型鏈。當前對象上不存在的屬性能夠經過原型鏈一層層往上查找,直到頂層 Object 對象,再往上就是 null

常見考點

  • 聊聊你理解的原型是什麼

繼承

即便是 ES6 中的 class 也不是其餘語言裏的類,本質就是一個函數。

class Person {}
Person instanceof Function // true

其實在當下都用 ES6 的狀況下,ES5 的繼承寫法已經沒啥學習的必要了,可是由於面試還會被問到,因此複習一下仍是須要的。

首先來講下 ES5 和 6 繼承的區別:

  1. ES6 繼承的子類須要調用 super() 才能拿到子類,ES5 的話是經過 apply 這種綁定的方式
  2. 類聲明不會提高,和 let 這些一致

接下來就是回字的幾種寫法的名場面了,ES5 實現繼承的方式有不少種,面試瞭解一種已經夠用:

function Super() {}
Super.prototype.getNumber = function() {
  return 1
}

function Sub() {}
Sub.prototype = Object.create(Super.prototype, {
  constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
let s = new Sub()
s.getNumber()

常見考點

  • JS 中如何實現繼承
  • 經過原型實現的繼承和 class 有何區別
  • 手寫任意一種原型繼承

深淺拷貝

淺拷貝

兩個對象第一層的引用不相同就是淺拷貝的含義。

咱們能夠經過 assign 、擴展運算符等方式來實現淺拷貝:

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
b = {...a}
a.age = 3
console.log(b.age) // 2

深拷貝

兩個對象內部全部的引用都不相同就是深拷貝的含義。

最簡單的深拷貝方式就是使用 JSON.parse(JSON.stringify(object)),可是該方法存在很多缺陷。

好比說只支持 JSON 支持的類型,JSON 是門通用的語言,並不支持 JS 中的全部類型。

同時還存在不能處理循環引用的問題:

若是想解決以上問題,咱們能夠經過遞歸的方式來實現代碼:

// 利用 WeakMap 解決循環引用
let map = new WeakMap()
function deepClone(obj) {
  if (obj instanceof Object) {
    if (map.has(obj)) {
      return map.get(obj)
    }
    let newObj
    if (obj instanceof Array) {
      newObj = []     
    } else if (obj instanceof Function) {
      newObj = function() {
        return obj.apply(this, arguments)
      }
    } else if (obj instanceof RegExp) {
      // 拼接正則
      newobj = new RegExp(obj.source, obj.flags)
    } else if (obj instanceof Date) {
      newobj = new Date(obj)
    } else {
      newObj = {}
    }
    // 克隆一份對象出來
    let desc = Object.getOwnPropertyDescriptors(obj)
    let clone = Object.create(Object.getPrototypeOf(obj), desc)
    map.set(obj, clone)
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        newObj[key] = deepClone(obj[key])
      }
    }
    return newObj
  }
  return obj
}

上述代碼解決了常見的類型以及循環引用的問題,固然仍是一部分缺陷的,可是面試時候能寫出上面的代碼已經足夠了,剩下的能口述思路基本這道題就能拿到高分了。

好比說遞歸確定會存在爆棧的問題,由於執行棧的大小是有限制的,到必定數量棧就會爆掉。

所以遇到這種問題,咱們能夠經過遍歷的方式來改寫遞歸。這個就是如何寫層序遍歷(BFS)的問題了,經過數組來模擬執行棧就能解決爆棧問題,有興趣的讀者能夠諮詢查閱。

Promise

Promise 是一個高頻考點了,可是更多的是在筆試題中出現,概念題反倒基本沒有,可能是來問 Event loop 的。

對於這塊內容的複習咱們須要熟悉涉及到的全部 API,由於考題裏可能會問到 allrace 等等用法或者須要你用這些 API 實現一些功能。

對於 Promise 進階點的知識能夠具體閱讀筆者的這篇文章,這裏就不復制過來佔用篇幅了:Promise 你真的用明白了麼?

常見考點

另外還有一道很常見的串行題目:

頁面上有三個按鈕,分別爲 A、B、C,點擊各個按鈕都會發送異步請求且互不影響,每次請求回來的數據都爲按鈕的名字。 請實現當用戶依次點擊 A、B、C、A、C、B 的時候,最終獲取的數據爲 ABCACB。

這道題目主要兩個考點:

  1. 請求不能阻塞,可是輸出能夠阻塞。好比說 B 請求須要耗時 3 秒,其餘請求耗時 1 秒,那麼當用戶點擊 BAC 時,三個請求都應該發起,可是由於 B 請求回來的慢,因此得等着輸出結果。
  2. 如何實現一個隊列?

其實咱們無需本身去構建一個隊列,直接利用 promise.then 方法就能實現隊列的效果了。

class Queue {
  promise = Promise.resolve();

  excute(promise) {
    this.promise = this.promise.then(() => promise);
    return this.promise;
  }
}

const queue = new Queue();

const delay = (params) => {
  const time = Math.floor(Math.random() * 5);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(params);
    }, time * 500);
  });
};

const handleClick = async (name) => {
  const res = await queue.excute(delay(name));
  console.log(res);
};

handleClick('A');
handleClick('B');
handleClick('C');
handleClick('A');
handleClick('C');
handleClick('B');

async、await

awaitpromise 同樣,更多的是考筆試題,固然偶爾也會問到和 promise 的一些區別。

await 相比直接使用 Promise 來講,優點在於處理 then 的調用鏈,可以更清晰準確的寫出代碼。缺點在於濫用 await 可能會致使性能問題,由於 await 會阻塞代碼,也許以後的異步代碼並不依賴於前者,但仍然須要等待前者完成,致使代碼失去了併發性,此時更應該使用 Promise.all

下面來看一道很容易作錯的筆試題。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> ?
}
b()
a++
console.log('1', a) // -> ?

這道題目大部分讀者確定會想到 await 左邊是異步代碼,所以會先把同步代碼執行完,此時 a 已經變成 1,因此答案應該是 11。

其實 a 爲 0 是由於加法運算法,先算左邊再算右邊,因此會把 0 固定下來。若是咱們把題目改爲 await 10 + a 的話,答案就是 11 了。

事件循環

在開始講事件循環以前,咱們必定要牢記一點:JS 是一門單線程語言,在執行過程當中永遠只能同時執行一個任務,任何異步的調用都只是在模擬這個過程,或者說能夠直接認爲在 JS 中的異步就是延遲執行的同步代碼。另外別的什麼 Web worker、瀏覽器提供的各類線程都不會影響這個點。

你們應該都知道執行 JS 代碼就是往執行棧裏 push 函數(不知道的本身搜索吧),那麼當遇到異步代碼的時候會發生什麼狀況?

其實當遇到異步的代碼時,只有當遇到 Task、Microtask 的時候纔會被掛起並在須要執行的時候加入到 Task(有多種 Task) 隊列中。

從圖上咱們得出兩個疑問:

  1. 什麼任務會被丟到 Microtask Queue 和 Task Queue 中?它們分別表明了什麼?
  2. Event loop 是如何處理這些 task 的?

首先咱們來解決問題一。

Task(宏任務):同步代碼、setTimeout 回調、setInteval 回調、IO、UI 交互事件、postMessageMessageChannel

MicroTask(微任務):Promise 狀態改變之後的回調函數(then 函數執行,若是此時狀態沒變,回調只會被緩存,只有當狀態改變,緩存的回調函數纔會被丟到任務隊列)、Mutation observer 回調函數、queueMicrotask 回調函數(新增的 API)。

宏任務會被丟到下一次事件循環,而且宏任務隊列每次只會執行一個任務。

微任務會被丟到本次事件循環,而且微任務隊列每次都會執行任務直到隊列爲空。

假如每一個微任務都會產生一個微任務,那麼宏任務永遠都不會被執行了。

接下來咱們來解決問題二。

Event Loop 執行順序以下所示:

  1. 執行同步代碼
  2. 執行完全部同步代碼後且執行棧爲空,判斷是否有微任務須要執行
  3. 執行全部微任務且微任務隊列爲空
  4. 是否有必要渲染頁面
  5. 執行一個宏任務

若是你以爲上面的表述不大理解的話,接下來咱們經過代碼示例來鞏固理解上面的知識:

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    queueMicrotask(() => console.log('queueMicrotask'))
    console.log('promise');
});

console.log('script end');
  1. 遇到 console.log 執行並打印
  2. 遇到 setTimeout,將回調加入宏任務隊列
  3. 遇到 Promise.resolve(),此時狀態已經改變,所以將 then 回調加入微任務隊列
  4. 遇到 console.log 執行並打印

此時同步任務所有執行完畢,分別打印了 'script start' 以及 'script end',開始判斷是否有微任務須要執行。

  1. 微任務隊列存在任務,開始執行 then 回調函數
  2. 遇到 queueMicrotask,將回到加入微任務隊列
  3. 遇到 console.log 執行並打印
  4. 檢查發現微任務隊列存在任務,執行 queueMicrotask 回調
  5. 遇到 console.log 執行並打印

此時發現微任務隊列已經清空,判斷是否須要進行 UI 渲染。

  1. 執行宏任務,開始執行 setTimeout 回調
  2. 遇到 console.log 執行並打印

執行一個宏任務即結束,尋找是否存在微任務,開始循環判斷...

其實事件循環沒啥難懂的,理解 JS 是個單線程語言,明白哪些是微宏任務、循環的順序就行了。

最後須要注意的一點:正是由於 JS 是門單線程語言,只能同時執行一個任務。所以全部的任務均可能由於以前任務的執行時間過長而被延遲執行,尤爲對於一些定時器而言。

常見考點

  • 什麼是事件循環?
  • JS 的執行原理?
  • 哪些是微宏任務?
  • 定時器是準時的嘛?

模塊化

當下模塊化主要就是 CommonJS 和 ES6 的 ESM 了,其它什麼的 AMD、UMD 瞭解下就好了。

ESM 我想應該沒啥好說的了,主要咱們來聊聊 CommonJS 以及 ESM 和 CommonJS 的區別。

CommonJS

CommonJs 是 Node 獨有的規範,固然 Webpack 也本身實現了這套東西,讓咱們能在瀏覽器裏跑起來這個規範。

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代碼中,module.exportsexports 很容易混淆,讓咱們來看看大體內部實現

// 基本實現
var module = {
  exports: {} // exports 就是個空對象
}
// 這個是爲何 exports 和 module.exports 用法類似的緣由
var exports = module.exports
var load = function (module) {
    // 導出的東西
    var a = 1
    module.exports = a
    return module.exports
};

根據上面的大體實現,咱們也能看出爲何對 exports 直接賦值不會有任何效果。

對於 CommonJS 和 ESM 的二者區別是:

  • 前者支持動態導入,也就是 require(${path}/xx.js),後者使用 import()
  • 前者是同步導入,由於用於服務端,文件都在本地,同步導入即便卡住主線程影響也不大。然後者是異步導入,由於用於瀏覽器,須要下載文件,若是也採用同步導入會對渲染有很大影響
  • 前者在導出時都是值拷貝,就算導出的值變了,導入的值也不會改變,因此若是想更新值,必須從新導入一次。可是後者採用實時綁定的方式,導入導出的值都指向同一個內存地址,因此導入值會跟隨導出值變化

垃圾回收

本小結內容創建在 V8 引擎之上。

首先聊垃圾回收以前咱們須要知道堆棧究竟是存儲什麼數據的,固然這塊內容上文已經講過,這裏就再也不贅述了。

接下來咱們先來聊聊棧是如何垃圾回收的。其實棧的回收很簡單,簡單來講就是一個函數 push 進棧,執行完畢之後 pop 出來就當能夠回收了。固然咱們往深層了講深層了講就是彙編裏的東西了,操做 esp 和 ebp 指針,瞭解下便可。

而後就是堆如何回收垃圾了,這部分的話會分爲兩個空間及多個算法。

兩個空間分別爲新生代和老生代,咱們分開來說每一個空間中涉及到的算法。

新生代

新生代中的對象通常存活時間較短,空間也較小,使用 Scavenge GC 算法。

在新生代空間中,內存空間分爲兩部分,分別爲 From 空間和 To 空間。在這兩個空間中,一定有一個空間是使用的,另外一個空間是空閒的。新分配的對象會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啓動了。算法會檢查 From 空間中存活的對象並複製到 To 空間中,若是有失活的對象就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

老生代

老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除和標記壓縮算法。

在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:

  • 新生代中的對象是否已經經歷過一次以上 Scavenge 算法,若是經歷過的話,會將對象重新生代空間移到老生代空間中。
  • To 空間的對象佔比大小超過 25 %。在這種狀況下,爲了避免影響到內存分配,會將對象重新生代空間移到老生代空間中。

老生代中的空間很複雜,有以下幾個空間

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不變的對象空間
  NEW_SPACE,   // 新生代用於 GC 複製算法的空間
  OLD_SPACE,   // 老生代常駐對象空間
  CODE_SPACE,  // 老生代代碼對象空間
  MAP_SPACE,   // 老生代 map 對象
  LO_SPACE,    // 老生代大空間對象
  NEW_LO_SPACE,  // 新生代大空間對象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,如下狀況會先啓動標記清除算法:

  • 某一個空間沒有分塊的時候
  • 空間中被對象超過必定限制
  • 空間不能保證新生代中的對象移動到老生代中

在這個階段中,會遍歷堆中全部的對象,而後標記活的對象,在標記完成後,銷燬全部沒有被標記的對象。在標記大型對內存時,可能須要幾百毫秒才能完成一次標記。這就會致使一些性能上的問題。爲了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工做分解爲更小的模塊,可讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓狀況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名爲併發標記。該技術可讓 GC 掃描和標記對象時,同時容許 JS 運行,你能夠點擊 該博客 詳細閱讀。

清除對象後會形成堆內存出現碎片的狀況,當碎片超過必定限制後會啓動壓縮算法。在壓縮過程當中,將活的對象像一端移動,直到全部對象都移動完成而後清理掉不須要的內存。

其它考點

0.1 + 0.2 !== 0.3

由於 JS 採用 IEEE 754 雙精度版本(64位),而且只要採用 IEEE 754 的語言都有該問題。

不止 0.1 + 0.2 存在問題,0.7 + 0.一、0.2 + 0.4 一樣也存在問題。

存在問題的緣由是浮點數用二進制表示的時候是無窮的,由於精度的問題,兩個浮點數相加會形成截斷丟失精度,所以再轉換爲十進制就出了問題。

解決的辦法能夠經過如下代碼:

export const addNum = (num1: number, num2: number) => {
  let sq1;
  let sq2;
  let m;
  try {
    sq1 = num1.toString().split('.')[1].length;
  } catch (e) {
    sq1 = 0;
  }
  try {
    sq2 = num2.toString().split('.')[1].length;
  } catch (e) {
    sq2 = 0;
  }
  m = Math.pow(10, Math.max(sq1, sq2));
  return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
};

核心就是計算出兩個浮點數最大的小數長度,好比說 0.1 + 0.22 的小數最大長度爲 2,而後兩數乘上 10 的 2次冪再相加得出數字 32,而後除以 10 的 2次冪便可得出正確答案 0.32。

手寫題

防抖

你是否在平常開發中遇到一個問題,在滾動事件中須要作個複雜計算或者實現一個按鈕的防二次點擊操做。

這些需求均可以經過函數防抖動來實現。尤爲是第一個需求,若是在頻繁的事件回調中作複雜計算,頗有可能致使頁面卡頓,不如將屢次計算合併爲一次計算,只在一個精確點作操做。

PS:防抖和節流的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於閾值,防抖的狀況下只會調用一次,而節流會每隔必定時間調用函數。

咱們先來看一個袖珍版的防抖理解一下防抖的實現:

// func是用戶傳入須要防抖的函數
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這裏返回的函數是每次用戶實際調用的防抖函數
  // 若是已經設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器,延遲執行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出若是用戶調用該函數的間隔小於 wait 的狀況下,上一次的時間還未到就被清除了,並不會執行函數

這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:

  • 例如在搜索引擎搜索問題的時候,咱們固然是但願用戶輸入完最後一個字才調用查詢接口,這個時候適用延遲執行的防抖函數,它老是在一連串(間隔小於wait的)函數觸發以後調用。
  • 例如用戶給interviewMap點star的時候,咱們但願用戶點第一下的時候就去調用接口,而且成功以後改變star按鈕的樣子,用戶就能夠立馬獲得反饋是否star成功了,這個狀況適用當即執行的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於wait纔會觸發。

下面咱們來實現一個帶有當即執行選項的防抖函數

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否當即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的狀況下,函數會在延遲函數中執行
    // 使用到以前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這裏返回的函數是每次實際調用的函數
  return function(...params) {
    // 若是沒有建立延遲執行函數(later),就建立一個
    if (!timer) {
      timer = later()
      // 若是是當即執行,調用函數
      // 不然緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個
    // 這樣作延遲函數會從新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

總體函數實現的不難,總結一下。

  • 對於按鈕防點擊來講的實現:若是函數是當即執行的,就當即調用,若是函數是延遲執行的,就緩存上下文和參數,放到延遲函數中去執行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都從新計時。一旦你點累了,定時器時間到,定時器重置爲 null,就能夠再次點擊了。
  • 對於延時執行函數來講的實現:清除定時器ID,若是是延遲調用就調用函數

節流

防抖動和節流本質是不同的。防抖動是將屢次執行變爲最後一次執行,節流是將屢次執行變成每隔一段時間執行。

/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   若是想忽略開始函數的的調用,傳入{leading: false}。
 *                                若是想忽略結尾函數的調用,傳入{trailing: false}
 *                                二者不能共存,不然函數不能執行
 * @return {function}             返回客戶調用函數   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 以前的時間戳
    var previous = 0;
    // 若是 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 若是設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 得到當前時間戳
      var now = _.now();
      // 首次進入前者確定爲 true
      // 若是須要第一次不執行函數
      // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 若是當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
       // 若是設置了 trailing,只會進入這個條件
      // 若是沒有設置 leading,那麼第一次會進入這個條件
      // 還有一點,你可能會以爲開啓了定時器那麼應該不會進入這個 if 條件了
      // 其實仍是會進入的,由於定時器的延時
      // 並非準確的時間,極可能你設置了2秒
      // 可是他須要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 若是存在定時器就清理掉不然會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
        // 沒有的話就開啓一個定時器
        // 而且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

Event Bus

class Events {
  constructor() {
    this.events = new Map();
  }

  addEvent(key, fn, isOnce, ...args) {
    const value = this.events.get(key) ? this.events.get(key) : this.events.set(key, new Map()).get(key)
    value.set(fn, (...args1) => {
        fn(...args, ...args1)
        isOnce && this.off(key, fn)
    })
  }

  on(key, fn, ...args) {
    if (!fn) {
      console.error(`沒有傳入回調函數`);
      return
    }
    this.addEvent(key, fn, false, ...args)
  }

  fire(key, ...args) {
    if (!this.events.get(key)) {
      console.warn(`沒有 ${key} 事件`);
      return;
    }
    for (let [, cb] of this.events.get(key).entries()) {
      cb(...args);
    }
  }

  off(key, fn) {
    if (this.events.get(key)) {
      this.events.get(key).delete(fn);
    }
  }

  once(key, fn, ...args) {
    this.addEvent(key, fn, true, ...args)
  }
}

instanceof

instanceof 能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 prototype

function instanceof(left, right) {
    // 得到類型的原型
    let prototype = right.prototype
    // 得到對象的原型
    left = left.__proto__
    // 判斷對象的類型是否等於類型的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

call

Function.prototype.myCall = function(context, ...args) {
  context = context || window
  let fn = Symbol()
  context[fn] = this
  let result = context[fn](...args)
  delete context[fn]
  return result
}

apply

Function.prototype.myApply = function(context) {
  context = context || window
  let fn = Symbol()
  context[fn] = this
  let result
  if (arguments[1]) {
    result = context[fn](...arguments[1])
  } else {
    result = context[fn]()
  }
  delete context[fn]
  return result
}

bind

Function.prototype.myBind = function (context) {
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一個函數
  return function F() {
    // 由於返回了一個函數,咱們能夠 new F(),因此須要判斷
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

其餘

其餘手寫題上文已經有說起,好比模擬 new、ES5 實現繼承、深拷貝。

另外你們可能常常能看到手寫 Promise 的文章,其實根據筆者目前收集到的數百道面試題以及讀者的反饋來看,壓根就沒人遇到這個考點,因此咱們大可沒必要在這上面花時間。

最後

以上就是本篇基礎的所有內容了,若是有各位讀者認爲重要的知識點筆者卻遺漏的話,歡迎你們指出。

你們也能夠在筆者的 網站上閱讀,體驗更佳!
相關文章
相關標籤/搜索