筆者開源的前端進階之道已有三年之久,至今也有 17k star,承蒙各位讀者垂愛。在當下部份內容已經略微過期,所以決定提筆翻新內容。html
翻新後的內容會所有集合在「幹爆前端」中,有興趣的讀者能夠前往查看。前端
閱讀前重要提示:c++
本文非百科全書,只專爲面試複習準備、查漏補缺、深刻某知識點的引子、瞭解相關面試題等準備。git
筆者一直都是崇尚學會面試題底下涉及到的知識點,而不是刷一大堆面試題,結果變了個題型就不會的那種。因此本文和別的面經不同,旨在提煉面試題底下的經常使用知識點,而不是甩一大堆面試題給各位看官。github
你們也能夠在筆者的 網站上閱讀,體驗更佳!
JS 數據類型分爲兩大類,九個數據類型:面試
其中原始類型又分爲七種類型,分別爲:算法
boolean
number
string
undefined
null
symbol
bigint
對象類型分爲兩種,分別爲:windows
Object
Function
其中 Object
中又包含了不少子類型,好比 Array
、RegExp
、Math
、Map
、Set
等等,也就不一一列出了。數組
原始類型存儲在棧上,對象類型存儲在堆上,可是它的引用地址仍是存在棧上。promise
注意:以上結論前半句是不許確的,更準確的內容我會在閉包章節裏說明。
bigint
,固然再加上字符串的處理會更好。NaN
如何判斷另外還有一類常見的題目是對於對象的修改,好比說往函數裏傳一個對象進去,函數內部修改參數。
function test(person) { person.age = 26 person = {} return person } const p1 = { age: 25 }
這類題目咱們只須要牢記如下幾點:
類型判斷有好幾種方式。
原始類型中除了 null
,其它類型均可以經過 typeof
來判斷。
typeof null
的值爲 object
,這是由於一個久遠的 Bug,沒有細究的必要,瞭解便可。若是想具體判斷 null
類型的話直接 xxx === null
便可。
對於對象類型來講,typeof
只能具體判斷函數的類型爲 function
,其它均爲 object
。
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 XXX]
中的 XXX
就是判斷出來的類型。
同時還存在一些判斷特定類型的 API,選了兩個常見的:
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
爲 0null
爲 0,undefined
爲 NaN
,symbol
報錯NaN
隱式轉換規則是最煩的,其實筆者也記不住那麼多內容。何況根據筆者目前收集到的最新面試題來講,這部分考題基本絕跡了,固然講仍是講一下吧。
對象轉基本類型:
Symbol.toPrimitive
,轉成功就結束valueOf
,轉成功就結束toString
,轉成功就結束四則運算符:
==
操做符
若是這部分規則記不住也不礙事,確實有點繁瑣,並且考的也愈來愈少了,拿一道之前常考的題目看看吧:
[] == ![] // -> ?
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
操做符能夠幫助咱們構建出一個實例,而且綁定上 this
,內部執行步驟可大概分爲如下幾步:
在第四步返回新對象這邊有一個狀況會例外:
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
的實現過程做用域能夠理解爲變量的可訪問性,總共分爲三種類型,分別爲:
let
、const
就能夠產生該做用域其實看完前面的閉包、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 繼承的區別:
super()
才能拿到子類,ES5 的話是經過 apply
這種綁定的方式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()
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
是一個高頻考點了,可是更多的是在筆試題中出現,概念題反倒基本沒有,可能是來問 Event loop 的。
對於這塊內容的複習咱們須要熟悉涉及到的全部 API,由於考題裏可能會問到 all
、race
等等用法或者須要你用這些 API 實現一些功能。
對於 Promise
進階點的知識能夠具體閱讀筆者的這篇文章,這裏就不復制過來佔用篇幅了:Promise 你真的用明白了麼?
all
實現並行需求all
的實現另外還有一道很常見的串行題目:
頁面上有三個按鈕,分別爲 A、B、C,點擊各個按鈕都會發送異步請求且互不影響,每次請求回來的數據都爲按鈕的名字。 請實現當用戶依次點擊 A、B、C、A、C、B 的時候,最終獲取的數據爲 ABCACB。
這道題目主要兩個考點:
其實咱們無需本身去構建一個隊列,直接利用 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');
await
和 promise
同樣,更多的是考筆試題,固然偶爾也會問到和 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) 隊列中。
從圖上咱們得出兩個疑問:
首先咱們來解決問題一。
Task(宏任務):同步代碼、setTimeout
回調、setInteval
回調、IO、UI 交互事件、postMessage
、MessageChannel
。
MicroTask(微任務):Promise
狀態改變之後的回調函數(then
函數執行,若是此時狀態沒變,回調只會被緩存,只有當狀態改變,緩存的回調函數纔會被丟到任務隊列)、Mutation observer
回調函數、queueMicrotask
回調函數(新增的 API)。
宏任務會被丟到下一次事件循環,而且宏任務隊列每次只會執行一個任務。
微任務會被丟到本次事件循環,而且微任務隊列每次都會執行任務直到隊列爲空。
假如每一個微任務都會產生一個微任務,那麼宏任務永遠都不會被執行了。
接下來咱們來解決問題二。
Event Loop 執行順序以下所示:
若是你以爲上面的表述不大理解的話,接下來咱們經過代碼示例來鞏固理解上面的知識:
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');
console.log
執行並打印setTimeout
,將回調加入宏任務隊列Promise.resolve()
,此時狀態已經改變,所以將 then
回調加入微任務隊列console.log
執行並打印此時同步任務所有執行完畢,分別打印了 'script start' 以及 'script end',開始判斷是否有微任務須要執行。
then
回調函數queueMicrotask
,將回到加入微任務隊列console.log
執行並打印queueMicrotask
回調console.log
執行並打印此時發現微任務隊列已經清空,判斷是否須要進行 UI 渲染。
setTimeout
回調console.log
執行並打印執行一個宏任務即結束,尋找是否存在微任務,開始循環判斷...
其實事件循環沒啥難懂的,理解 JS 是個單線程語言,明白哪些是微宏任務、循環的順序就行了。
最後須要注意的一點:正是由於 JS 是門單線程語言,只能同時執行一個任務。所以全部的任務均可能由於以前任務的執行時間過長而被延遲執行,尤爲對於一些定時器而言。
當下模塊化主要就是 CommonJS 和 ES6 的 ESM 了,其它什麼的 AMD、UMD 瞭解下就好了。
ESM 我想應該沒啥好說的了,主要咱們來聊聊 CommonJS 以及 ESM 和 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.exports
和 exports
很容易混淆,讓咱們來看看大體內部實現
// 基本實現 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 就結束了。
老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除和標記壓縮算法。
在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:
老生代中的空間很複雜,有以下幾個空間
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 運行,你能夠點擊 該博客 詳細閱讀。
清除對象後會形成堆內存出現碎片的狀況,當碎片超過必定限制後會啓動壓縮算法。在壓縮過程當中,將活的對象像一端移動,直到全部對象都移動完成而後清理掉不須要的內存。
由於 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纔會觸發。下面咱們來實現一個帶有當即執行選項的防抖函數
// 這個是用來獲取當前時間戳的 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
,就能夠再次點擊了。防抖動和節流本質是不同的。防抖動是將屢次執行變爲最後一次執行,節流是將屢次執行變成每隔一段時間執行。
/** * 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; }; };
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
能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 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__ } }
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 }
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 }
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 的文章,其實根據筆者目前收集到的數百道面試題以及讀者的反饋來看,壓根就沒人遇到這個考點,因此咱們大可沒必要在這上面花時間。
以上就是本篇基礎的所有內容了,若是有各位讀者認爲重要的知識點筆者卻遺漏的話,歡迎你們指出。
你們也能夠在筆者的 網站上閱讀,體驗更佳!