閱讀前重要提示:
本文非百科全書(shū),只專(zhuān)為面試復(fù)習(xí)準(zhǔn)備、查漏補(bǔ)缺、深入某知識(shí)點(diǎn)的引子、了解相關(guān)面試題等準(zhǔn)備。
https://github.com/KieSun/fucking-frontend
筆者一直都是崇尚學(xué)會(huì)面試題底下涉及到的知識(shí)點(diǎn),而不是刷一大堆面試題,結(jié)果變了個(gè)題型就不會(huì)的那種。所以本文和別的面經(jīng)不一樣,旨在提煉面試題底下的常用知識(shí)點(diǎn),而不是甩一大堆面試題給各位看官。
數(shù)據(jù)類(lèi)型
JS 數(shù)據(jù)類(lèi)型分為兩大類(lèi),九個(gè)數(shù)據(jù)類(lèi)型:
- 原始類(lèi)型
- 對(duì)象類(lèi)型
其中原始類(lèi)型又分為七種類(lèi)型,分別為:
- boolean
- number
- string
- undefined
- null
- symbol
- bigint
對(duì)象類(lèi)型分為兩種,分別為:
- Object
- Function
其中 Object 中又包含了很多子類(lèi)型,比如 Array、RegExp、Math、Map、Set 等等,也就不一一列出了。
原始類(lèi)型存儲(chǔ)在棧上,對(duì)象類(lèi)型存儲(chǔ)在堆上,但是它的引用地址還是存在棧上。
注意:以上結(jié)論前半句是不準(zhǔn)確的,更準(zhǔn)確的內(nèi)容我會(huì)在閉包章節(jié)里說(shuō)明。
常見(jiàn)考點(diǎn)
- JS 類(lèi)型有哪些?
- 大數(shù)相加、相乘算法題,可以直接使用 bigint,當(dāng)然再加上字符串的處理會(huì)更好。
- NaN 如何判斷
另外還有一類(lèi)常見(jiàn)的題目是對(duì)于對(duì)象的修改,比如說(shuō)往函數(shù)里傳一個(gè)對(duì)象進(jìn)去,函數(shù)內(nèi)部修改參數(shù)。
function test(person) {
person.age = 26
person = {}
return person
}
const p1 = {
age: 25
}
這類(lèi)題目我們只需要牢記以下幾點(diǎn):
- 對(duì)象存儲(chǔ)的是引用地址,傳來(lái)傳去、賦值給別人那都是在傳遞值(存在棧上的那個(gè)內(nèi)容),別人一旦修改對(duì)象里的屬性,大家都被修改了。
- 但是一旦對(duì)象被重新賦值了,只要不是原對(duì)象被重新賦值,那么就永遠(yuǎn)不會(huì)修改原對(duì)象。
類(lèi)型判斷
類(lèi)型判斷有好幾種方式。
typeof
原始類(lèi)型中除了 null,其它類(lèi)型都可以通過(guò) typeof 來(lái)判斷。
typeof null 的值為 object,這是因?yàn)橐粋€(gè)久遠(yuǎn)的 Bug,沒(méi)有細(xì)究的必要,了解即可。如果想具體判斷 null 類(lèi)型的話(huà)直接 xxx === null 即可。
對(duì)于對(duì)象類(lèi)型來(lái)說(shuō),typeof 只能具體判斷函數(shù)的類(lèi)型為 function,其它均為 object。
instanceof
instanceof 內(nèi)部通過(guò)原型鏈的方式來(lái)判斷是否為構(gòu)建函數(shù)的實(shí)例,常用于判斷具體的對(duì)象類(lèi)型。
[] instanceof Array
都說(shuō) instanceof 只能判斷對(duì)象類(lèi)型,其實(shí)這個(gè)說(shuō)法是不準(zhǔn)確的,我們是可以通過(guò) hake 的方式得以實(shí)現(xiàn),雖然不會(huì)有人這樣去玩吧。
class CheckIsNumber {
static [Symbol.hasInstance](number) {
return typeof number === 'number'
}
}
// true
1 instanceof CheckIsNumber
另外其實(shí)我們還可以直接通過(guò)構(gòu)建函數(shù)來(lái)判斷類(lèi)型:
// true
[].constructor === Array
Object.prototype.toString
前幾種方式或多或少都存在一些缺陷,Object.prototype.toString 綜合來(lái)看是最佳選擇,能判斷的類(lèi)型最完整。
上圖是一部分類(lèi)型判斷,更多的就不列舉了,[object XXX] 中的 XXX 就是判斷出來(lái)的類(lèi)型。
isXXX API
同時(shí)還存在一些判斷特定類(lèi)型的 API,選了兩個(gè)常見(jiàn)的:
常見(jiàn)考點(diǎn)
- JS 類(lèi)型如何判斷,有哪幾種方式可用
- instanceof 原理
- 手寫(xiě) instanceof
類(lèi)型轉(zhuǎn)換
類(lèi)型轉(zhuǎn)換分為兩種情況,分別為強(qiáng)制轉(zhuǎn)換及隱式轉(zhuǎn)換。
強(qiáng)制轉(zhuǎn)換
強(qiáng)制轉(zhuǎn)換就是轉(zhuǎn)成特定的類(lèi)型:
Number(false) // -> 0
Number('1') // -> 1
Number('zb') // -> NaN
(1).toString() // '1'
這部分是日常常用的內(nèi)容,就不具體展開(kāi)說(shuō)了,主要記住強(qiáng)制轉(zhuǎn)數(shù)字和布爾值的規(guī)則就行。
轉(zhuǎn)布爾值規(guī)則:
- undefined、null、false、NaN、''、0、-0 都轉(zhuǎn)為 false。
- 其他所有值都轉(zhuǎn)為 true,包括所有對(duì)象。
轉(zhuǎn)數(shù)字規(guī)則:
- true 為 1,false 為 0
- null 為 0,undefined 為 NaN,symbol 報(bào)錯(cuò)
- 字符串看內(nèi)容,如果是數(shù)字或者進(jìn)制值就正常轉(zhuǎn),否則就 NaN
- 對(duì)象的規(guī)則隱式轉(zhuǎn)換再講
隱式轉(zhuǎn)換
隱式轉(zhuǎn)換規(guī)則是最煩的,其實(shí)筆者也記不住那么多內(nèi)容。況且根據(jù)筆者目前收集到的最新面試題來(lái)說(shuō),這部分考題基本絕跡了,當(dāng)然講還是講一下吧。
對(duì)象轉(zhuǎn)基本類(lèi)型:
- 調(diào)用 Symbol.toPrimitive,轉(zhuǎn)成功就結(jié)束
- 調(diào)用 valueOf,轉(zhuǎn)成功就結(jié)束
- 調(diào)用 toString,轉(zhuǎn)成功就結(jié)束
- 報(bào)錯(cuò)
四則運(yùn)算符:
- 只有當(dāng)加法運(yùn)算時(shí),其中一方是字符串類(lèi)型,就會(huì)把另一個(gè)也轉(zhuǎn)為字符串類(lèi)型
- 其他運(yùn)算只要其中一方是數(shù)字,那么另一方就轉(zhuǎn)為數(shù)字
== 操作符
常見(jiàn)考點(diǎn)
如果這部分規(guī)則記不住也不礙事,確實(shí)有點(diǎn)繁瑣,而且考得也越來(lái)越少了,拿一道以前常考的題目看看吧:
[] == ![] // -> ?
this
this 是很多人會(huì)混淆的概念,但是其實(shí)他一點(diǎn)都不難,不要被那些長(zhǎng)篇大論的文章嚇住了(我其實(shí)也不知道為什么他們能寫(xiě)那么多字),你只需要記住幾個(gè)規(guī)則就可以了。
普通函數(shù)
function foo() {
console.log(this.a)
}
var a = 1
foo()
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上情況就是看函數(shù)是被誰(shuí)調(diào)用,那么 `this` 就是誰(shuí),沒(méi)有被對(duì)象調(diào)用,`this` 就是 `window`
// 以下情況是優(yōu)先級(jí)最高的,`this` 只會(huì)綁定在 `c` 上,不會(huì)被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 還有種就是利用 call,Apply,bind 改變 this,這個(gè)優(yōu)先級(jí)僅次于 new
箭頭函數(shù)
因?yàn)榧^函數(shù)沒(méi)有 this,所以一切妄圖改變箭頭函數(shù) this 指向都是無(wú)效的。
箭頭函數(shù)的 this 只取決于定義時(shí)的環(huán)境。比如如下代碼中的 fn 箭頭函數(shù)是在 windows 環(huán)境下定義的,無(wú)論如何調(diào)用,this 都指向 window。
var a = 1
const fn = () => {
console.log(this.a)
}
const obj = {
fn,
a: 2
}
obj.fn()
常見(jiàn)考點(diǎn)
這里一般都是考 this 的指向問(wèn)題,牢記上述的幾個(gè)規(guī)則就夠用了,比如下面這道題:
const a = {
b: 2,
foo: function () { console.log(this.b) }
}
function b(foo) {
// 輸出什么?
foo()
}
b(a.foo)
閉包
首先閉包正確的定義是:假如一個(gè)函數(shù)能訪(fǎng)問(wèn)外部的變量,那么這個(gè)函數(shù)它就是一個(gè)閉包,而不是一定要返回一個(gè)函數(shù)。這個(gè)定義很重要,下面的內(nèi)容需要用到。
let a = 1
// fn 是閉包
function fn() {
console.log(a);
}
function fn1() {
let a = 1
// 這里也是閉包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
大家都知道閉包其中一個(gè)作用是訪(fǎng)問(wèn)私有變量,就比如上述代碼中的 fn2 訪(fǎng)問(wèn)到了 fn1 函數(shù)中的變量 a。但是此時(shí) fn1 早已銷(xiāo)毀,我們是如何訪(fǎng)問(wèn)到變量 a 的呢?不是都說(shuō)原始類(lèi)型是存放在棧上的么,為什么此時(shí)卻沒(méi)有被銷(xiāo)毀掉?
接下來(lái)筆者會(huì)根據(jù)瀏覽器的表現(xiàn)來(lái)重新理解關(guān)于原始類(lèi)型存放位置的說(shuō)法。
先來(lái)說(shuō)下數(shù)據(jù)存放的正確規(guī)則是:局部、占用空間確定的數(shù)據(jù),一般會(huì)存放在棧中,否則就在堆中(也有例外)。 那么接下來(lái)我們可以通過(guò) Chrome 來(lái)幫助我們驗(yàn)證這個(gè)說(shuō)法說(shuō)法。
上圖中畫(huà)紅框的位置我們能看到一個(gè)內(nèi)部的對(duì)象 [[Scopes]],其中存放著變量 a,該對(duì)象是被存放在堆上的,其中包含了閉包、全局對(duì)象等等內(nèi)容,因此我們能通過(guò)閉包訪(fǎng)問(wèn)到本該銷(xiāo)毀的變量。
另外最開(kāi)始我們對(duì)于閉包的定位是:假如一個(gè)函數(shù)能訪(fǎng)問(wèn)外部的變量,那么這個(gè)函數(shù)它就是一個(gè)閉包,因此接下來(lái)我們看看在全局下的表現(xiàn)是怎么樣的。
let a = 1
var b = 2
// fn 是閉包
function fn() {
console.log(a, b);
}
從上圖我們能發(fā)現(xiàn)全局下聲明的變量,如果是 var 的話(huà)就直接被掛到 globe 上,如果是其他關(guān)鍵字聲明的話(huà)就被掛到 Script 上。雖然這些內(nèi)容同樣還是存在 [[Scopes]],但是全局變量應(yīng)該是存放在靜態(tài)區(qū)域的,因?yàn)槿肿兞繜o(wú)需進(jìn)行垃圾回收,等需要回收的時(shí)候整個(gè)應(yīng)用都沒(méi)了。
只有在下圖的場(chǎng)景中,原始類(lèi)型才可能是被存儲(chǔ)在棧上。
這里為什么要說(shuō)可能,是因?yàn)?JS 是門(mén)動(dòng)態(tài)類(lèi)型語(yǔ)言,一個(gè)變量聲明時(shí)可以是原始類(lèi)型,馬上又可以賦值為對(duì)象類(lèi)型,然后又回到原始類(lèi)型。這樣頻繁的在堆棧上切換存儲(chǔ)位置,內(nèi)部引擎是不是也會(huì)有什么優(yōu)化手段,或者干脆全部都丟堆上?只有 const 聲明的原始類(lèi)型才一定存在棧上?當(dāng)然這只是筆者的一個(gè)推測(cè),暫時(shí)沒(méi)有深究,讀者可以忽略這段瞎想。
因此筆者對(duì)于原始類(lèi)型存儲(chǔ)位置的理解為:局部變量才是被存儲(chǔ)在棧上,全局變量存在靜態(tài)區(qū)域上,其它都存儲(chǔ)在堆上。
當(dāng)然這個(gè)理解是建立的 Chrome 的表現(xiàn)之上的,在不同的瀏覽器上因?yàn)橐娴牟煌赡艽鎯?chǔ)的方式還是有所變化的。
常見(jiàn)考點(diǎn)
閉包能考得很多,概念和筆試題都會(huì)考。
概念題就是考考閉包是什么了。
筆試題的話(huà)基本都會(huì)結(jié)合上異步,比如最常見(jiàn)的:
for (var i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
})
}
這道題會(huì)問(wèn)輸出什么,有哪幾種方式可以得到想要的答案?
new
new 操作符可以幫助我們構(gòu)建出一個(gè)實(shí)例,并且綁定上 this,內(nèi)部執(zhí)行步驟可大概分為以下幾步:
- 新生成了一個(gè)對(duì)象
- 對(duì)象連接到構(gòu)造函數(shù)原型上,并綁定 this
- 執(zhí)行構(gòu)造函數(shù)代碼
- 返回新對(duì)象
在第四步返回新對(duì)象這邊有一個(gè)情況會(huì)例外:
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'
當(dāng)在構(gòu)造函數(shù)中返回一個(gè)對(duì)象時(shí),內(nèi)部創(chuàng)建出來(lái)的新對(duì)象就被我們返回的對(duì)象所覆蓋,所以一般來(lái)說(shuō)構(gòu)建函數(shù)就別返回對(duì)象了(返回原始類(lèi)型不影響)。
常見(jiàn)考點(diǎn)
- new 做了哪些事?
- new 返回不同的類(lèi)型時(shí)會(huì)有什么表現(xiàn)?
- 手寫(xiě) new 的實(shí)現(xiàn)過(guò)程
作用域
作用域可以理解為變量的可訪(fǎng)問(wèn)性,總共分為三種類(lèi)型,分別為:
- 全局作用域
- 函數(shù)作用域
- 塊級(jí)作用域,ES6 中的 let、const 就可以產(chǎn)生該作用域
其實(shí)看完前面的閉包、this 這部分內(nèi)部的話(huà),應(yīng)該基本能了解作用域的一些應(yīng)用。
一旦我們將這些作用域嵌套起來(lái),就變成了另外一個(gè)重要的知識(shí)點(diǎn)「作用域鏈」,也就是 JS 到底是如何訪(fǎng)問(wèn)需要的變量或者函數(shù)的。
首先作用域鏈?zhǔn)窃诙x時(shí)就被確定下來(lái)的,和箭頭函數(shù)里的 this 一樣,后續(xù)不會(huì)改變,JS 會(huì)一層層往上尋找需要的內(nèi)容。
其實(shí)作用域鏈這個(gè)東西我們?cè)陂]包小結(jié)中已經(jīng)看到過(guò)它的實(shí)體了:[[Scopes]]
圖中的 [[Scopes]] 是個(gè)數(shù)組,作用域的一層層往上尋找就等同于遍歷 [[Scopes]]。
常見(jiàn)考點(diǎn)
- 什么是作用域
- 什么是作用域鏈
原型
原型在面試?yán)镏恍枰獛拙湓?huà)、一張圖的概念就夠用了,沒(méi)人會(huì)讓你長(zhǎng)篇大論講上一堆內(nèi)容的,問(wèn)原型更多的是為了引出繼承這個(gè)話(huà)題。
根據(jù)上圖,原型總結(jié)下來(lái)的概念為:
- 所有對(duì)象都有一個(gè)屬性 __proto__ 指向一個(gè)對(duì)象,也就是原型
- 每個(gè)對(duì)象的原型都可以通過(guò) constructor 找到構(gòu)造函數(shù),構(gòu)造函數(shù)也可以通過(guò) prototype 找到原型
- 所有函數(shù)都可以通過(guò) __proto__ 找到 Function 對(duì)象
- 所有對(duì)象都可以通過(guò) __proto__ 找到 Object 對(duì)象
- 對(duì)象之間通過(guò) __proto__ 連接起來(lái),這樣稱(chēng)之為原型鏈。當(dāng)前對(duì)象上不存在的屬性可以通過(guò)原型鏈一層層往上查找,直到頂層 Object 對(duì)象,再往上就是 null 了
常見(jiàn)考點(diǎn)
- 聊聊你理解的原型是什么
繼承
即使是 ES6 中的 class 也不是其他語(yǔ)言里的類(lèi),本質(zhì)就是一個(gè)函數(shù)。
class Person {}
Person instanceof Function // true
其實(shí)在當(dāng)下都用 ES6 的情況下,ES5 的繼承寫(xiě)法已經(jīng)沒(méi)啥學(xué)習(xí)的必要了,但是因?yàn)槊嬖囘€會(huì)被問(wèn)到,所以復(fù)習(xí)一下還是需要的。
首先來(lái)說(shuō)下 ES5 和 6 繼承的區(qū)別:
- ES6 繼承的子類(lèi)需要調(diào)用 super() 才能拿到子類(lèi),ES5 的話(huà)是通過(guò) apply 這種綁定的方式
- 類(lèi)聲明不會(huì)提升,和 let 這些一致
接下來(lái)就是回字的幾種寫(xiě)法的名場(chǎng)面了,ES5 實(shí)現(xiàn)繼承的方式有很多種,面試了解一種已經(jīng)夠用:
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()
常見(jiàn)考點(diǎn)
- JS 中如何實(shí)現(xiàn)繼承
- 通過(guò)原型實(shí)現(xiàn)的繼承和 class 有何區(qū)別
- 手寫(xiě)任意一種原型繼承
深淺拷貝
淺拷貝
兩個(gè)對(duì)象第一層的引用不相同就是淺拷貝的含義。
我們可以通過(guò) assign 、擴(kuò)展運(yùn)算符等方式來(lái)實(shí)現(xiàn)淺拷貝:
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
深拷貝
兩個(gè)對(duì)象內(nèi)部所有的引用都不相同就是深拷貝的含義。
最簡(jiǎn)單的深拷貝方式就是使用 JSON.parse(JSON.stringify(object)),但是該方法存在不少缺陷。
比如說(shuō)只支持 JSON 支持的類(lèi)型,JSON 是門(mén)通用的語(yǔ)言,并不支持 JS 中的所有類(lèi)型。
同時(shí)還存在不能處理循環(huán)引用的問(wèn)題:
如果想解決以上問(wèn)題,我們可以通過(guò)遞歸的方式來(lái)實(shí)現(xiàn)代碼:
// 利用 WeakMap 解決循環(huán)引用
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 = {}
}
// 克隆一份對(duì)象出來(lái)
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
}
上述代碼解決了常見(jiàn)的類(lèi)型以及循環(huán)引用的問(wèn)題,當(dāng)然還是一部分缺陷的,但是面試時(shí)候能寫(xiě)出上面的代碼已經(jīng)足夠了,剩下的能口述思路基本這道題就能拿到高分了。
比如說(shuō)遞歸肯定會(huì)存在爆棧的問(wèn)題,因?yàn)閳?zhí)行棧的大小是有限制的,到一定數(shù)量棧就會(huì)爆掉。
因此遇到這種問(wèn)題,我們可以通過(guò)遍歷的方式來(lái)改寫(xiě)遞歸。這個(gè)就是如何寫(xiě)層序遍歷(BFS)的問(wèn)題了,通過(guò)數(shù)組來(lái)模擬執(zhí)行棧就能解決爆棧問(wèn)題,有興趣的讀者可以咨詢(xún)查閱。
Promise
Promise 是一個(gè)高頻考點(diǎn)了,但是更多的是在筆試題中出現(xiàn),概念題反倒基本沒(méi)有,多是來(lái)問(wèn) Event loop 的。
對(duì)于這塊內(nèi)容的復(fù)習(xí)我們需要熟悉涉及到的所有 API,因?yàn)榭碱}里可能會(huì)問(wèn)到 all、race 等等用法或者需要你用這些 API 實(shí)現(xiàn)一些功能。
對(duì)于 Promise 進(jìn)階點(diǎn)的知識(shí)可以具體閱讀筆者的這篇文章,這里就不復(fù)制過(guò)來(lái)占用篇幅了:Promise 你真的弄明白了么?
常見(jiàn)考點(diǎn)
- 使用 all 實(shí)現(xiàn)并行需求
- Promise all 錯(cuò)誤處理
- 手寫(xiě) all 的實(shí)現(xiàn)
另外還有一道很常見(jiàn)的串行題目:
頁(yè)面上有三個(gè)按鈕,分別為 A、B、C,點(diǎn)擊各個(gè)按鈕都會(huì)發(fā)送異步請(qǐng)求且互不影響,每次請(qǐng)求回來(lái)的數(shù)據(jù)都為按鈕的名字。 請(qǐng)實(shí)現(xiàn)當(dāng)用戶(hù)依次點(diǎn)擊 A、B、C、A、C、B 的時(shí)候,最終獲取的數(shù)據(jù)為 ABCACB。
這道題目主要兩個(gè)考點(diǎn):
- 請(qǐng)求不能阻塞,但是輸出可以阻塞。比如說(shuō) B 請(qǐng)求需要耗時(shí) 3 秒,其他請(qǐng)求耗時(shí) 1 秒,那么當(dāng)用戶(hù)點(diǎn)擊 BAC 時(shí),三個(gè)請(qǐng)求都應(yīng)該發(fā)起,但是因?yàn)?B 請(qǐng)求回來(lái)的慢,所以得等著輸出結(jié)果。
- 如何實(shí)現(xiàn)一個(gè)隊(duì)列?
其實(shí)我們無(wú)需自己去構(gòu)建一個(gè)隊(duì)列,直接利用 promise.then 方法就能實(shí)現(xiàn)隊(duì)列的效果了。
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
await 和 promise 一樣,更多的是考筆試題,當(dāng)然偶爾也會(huì)問(wèn)到和 promise 的一些區(qū)別。
await 相比直接使用 Promise 來(lái)說(shuō),優(yōu)勢(shì)在于處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫(xiě)出代碼。缺點(diǎn)在于濫用 await 可能會(huì)導(dǎo)致性能問(wèn)題,因?yàn)?nbsp;await 會(huì)阻塞代碼,也許之后的異步代碼并不依賴(lài)于前者,但仍然需要等待前者完成,導(dǎo)致代碼失去了并發(fā)性,此時(shí)更應(yīng)該使用 Promise.all。
下面來(lái)看一道很容易做錯(cuò)的筆試題。
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> ?
}
b()
a++
console.log('1', a) // -> ?
這道題目大部分讀者肯定會(huì)想到 await 左邊是異步代碼,因此會(huì)先把同步代碼執(zhí)行完,此時(shí) a 已經(jīng)變成 1,所以答案應(yīng)該是 11。
其實(shí) a 為 0 是因?yàn)榧臃ㄟ\(yùn)算法,先算左邊再算右邊,所以會(huì)把 0 固定下來(lái)。如果我們把題目改成 await 10 + a 的話(huà),答案就是 11 了。
事件循環(huán)
在開(kāi)始講事件循環(huán)之前,我們一定要牢記一點(diǎn):JS 是一門(mén)單線(xiàn)程語(yǔ)言,在執(zhí)行過(guò)程中永遠(yuǎn)只能同時(shí)執(zhí)行一個(gè)任務(wù),任何異步的調(diào)用都只是在模擬這個(gè)過(guò)程,或者說(shuō)可以直接認(rèn)為在 JS 中的異步就是延遲執(zhí)行的同步代碼。另外別的什么 Web worker、瀏覽器提供的各種線(xiàn)程都不會(huì)影響這個(gè)點(diǎn)。
大家應(yīng)該都知道執(zhí)行 JS 代碼就是往執(zhí)行棧里 push 函數(shù)(不知道的自己搜索吧),那么當(dāng)遇到異步代碼的時(shí)候會(huì)發(fā)生什么情況?
其實(shí)當(dāng)遇到異步的代碼時(shí),只有當(dāng)遇到 Task、Microtask 的時(shí)候才會(huì)被掛起并在需要執(zhí)行的時(shí)候加入到 Task(有多種 Task) 隊(duì)列中。
從圖上我們得出兩個(gè)疑問(wèn):
- 什么任務(wù)會(huì)被丟到 Microtask Queue 和 Task Queue 中?它們分別代表了什么?
- Event loop 是如何處理這些 task 的?
首先我們來(lái)解決問(wèn)題一。
Task(宏任務(wù)):同步代碼、setTimeout 回調(diào)、setInteval 回調(diào)、IO、UI 交互事件、postMessage、MessageChannel。
MicroTask(微任務(wù)):Promise 狀態(tài)改變以后的回調(diào)函數(shù)(then 函數(shù)執(zhí)行,如果此時(shí)狀態(tài)沒(méi)變,回調(diào)只會(huì)被緩存,只有當(dāng)狀態(tài)改變,緩存的回調(diào)函數(shù)才會(huì)被丟到任務(wù)隊(duì)列)、Mutation observer 回調(diào)函數(shù)、queueMicrotask 回調(diào)函數(shù)(新增的 API)。
宏任務(wù)會(huì)被丟到下一次事件循環(huán),并且宏任務(wù)隊(duì)列每次只會(huì)執(zhí)行一個(gè)任務(wù)。
微任務(wù)會(huì)被丟到本次事件循環(huán),并且微任務(wù)隊(duì)列每次都會(huì)執(zhí)行任務(wù)直到隊(duì)列為空。
假如每個(gè)微任務(wù)都會(huì)產(chǎn)生一個(gè)微任務(wù),那么宏任務(wù)永遠(yuǎn)都不會(huì)被執(zhí)行了。
接下來(lái)我們來(lái)解決問(wèn)題二。
Event Loop 執(zhí)行順序如下所示:
- 執(zhí)行同步代碼
- 執(zhí)行完所有同步代碼后且執(zhí)行棧為空,判斷是否有微任務(wù)需要執(zhí)行
- 執(zhí)行所有微任務(wù)且微任務(wù)隊(duì)列為空
- 是否有必要渲染頁(yè)面
- 執(zhí)行一個(gè)宏任務(wù)
如果你覺(jué)得上面的表述不大理解的話(huà),接下來(lái)我們通過(guò)代碼示例來(lái)鞏固理解上面的知識(shí):
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 執(zhí)行并打印
- 遇到 setTimeout,將回調(diào)加入宏任務(wù)隊(duì)列
- 遇到 Promise.resolve(),此時(shí)狀態(tài)已經(jīng)改變,因此將 then 回調(diào)加入微任務(wù)隊(duì)列
- 遇到 console.log 執(zhí)行并打印
此時(shí)同步任務(wù)全部執(zhí)行完畢,分別打印了 'script start' 以及 'script end',開(kāi)始判斷是否有微任務(wù)需要執(zhí)行。
- 微任務(wù)隊(duì)列存在任務(wù),開(kāi)始執(zhí)行 then 回調(diào)函數(shù)
- 遇到 queueMicrotask,將回到加入微任務(wù)隊(duì)列
- 遇到 console.log 執(zhí)行并打印
- 檢查發(fā)現(xiàn)微任務(wù)隊(duì)列存在任務(wù),執(zhí)行 queueMicrotask 回調(diào)
- 遇到 console.log 執(zhí)行并打印
此時(shí)發(fā)現(xiàn)微任務(wù)隊(duì)列已經(jīng)清空,判斷是否需要進(jìn)行 UI 渲染。
- 執(zhí)行宏任務(wù),開(kāi)始執(zhí)行 setTimeout 回調(diào)
- 遇到 console.log 執(zhí)行并打印
執(zhí)行一個(gè)宏任務(wù)即結(jié)束,尋找是否存在微任務(wù),開(kāi)始循環(huán)判斷...
其實(shí)事件循環(huán)沒(méi)啥難懂的,理解 JS 是個(gè)單線(xiàn)程語(yǔ)言,明白哪些是微宏任務(wù)、循環(huán)的順序就好了。
最后需要注意的一點(diǎn):正是因?yàn)?JS 是門(mén)單線(xiàn)程語(yǔ)言,只能同時(shí)執(zhí)行一個(gè)任務(wù)。因此所有的任務(wù)都可能因?yàn)橹叭蝿?wù)的執(zhí)行時(shí)間過(guò)長(zhǎng)而被延遲執(zhí)行,尤其對(duì)于一些定時(shí)器而言。
常見(jiàn)考點(diǎn)
- 什么是事件循環(huán)?
- JS 的執(zhí)行原理?
- 哪些是微宏任務(wù)?
- 定時(shí)器是準(zhǔn)時(shí)的嗎?
模塊化
當(dāng)下模塊化主要就是 CommonJS 和 ES6 的 ESM 了,其它什么的 AMD、UMD 了解下就行了。
ESM 我想應(yīng)該沒(méi)啥好說(shuō)的了,主要我們來(lái)聊聊 CommonJS 以及 ESM 和 CommonJS 的區(qū)別。
CommonJS
CommonJs 是 Node 獨(dú)有的規(guī)范,當(dāng)然 Webpack 也自己實(shí)現(xiàn)了這套東西,讓我們能在瀏覽器里跑起來(lái)這個(gè)規(guī)范。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
在上述代碼中,module.exports 和 exports 很容易混淆,讓我們來(lái)看看大致內(nèi)部實(shí)現(xiàn)
// 基本實(shí)現(xiàn)
var module = {
exports: {} // exports 就是個(gè)空對(duì)象
}
// 這個(gè)是為什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 導(dǎo)出的東西
var a = 1
module.exports = a
return module.exports
};
根據(jù)上面的大致實(shí)現(xiàn),我們也能看出為什么對(duì) exports 直接賦值不會(huì)有任何效果。
對(duì)于 CommonJS 和 ESM 的兩者區(qū)別是:
- 前者支持動(dòng)態(tài)導(dǎo)入,也就是 require(${path}/xx.js),后者使用 import()
- 前者是同步導(dǎo)入,因?yàn)橛糜诜?wù)端,文件都在本地,同步導(dǎo)入即使卡住主線(xiàn)程影響也不大。而后者是異步導(dǎo)入,因?yàn)橛糜跒g覽器,需要下載文件,如果也采用同步導(dǎo)入會(huì)對(duì)渲染有很大影響
- 前者在導(dǎo)出時(shí)都是值拷貝,就算導(dǎo)出的值變了,導(dǎo)入的值也不會(huì)改變,所以如果想更新值,必須重新導(dǎo)入一次。但是后者采用實(shí)時(shí)綁定的方式,導(dǎo)入導(dǎo)出的值都指向同一個(gè)內(nèi)存地址,所以導(dǎo)入值會(huì)跟隨導(dǎo)出值變化
垃圾回收
本小結(jié)內(nèi)容建立在 V8 引擎之上。
首先聊垃圾回收之前我們需要知道堆棧到底是存儲(chǔ)什么數(shù)據(jù)的,當(dāng)然這塊內(nèi)容上文已經(jīng)講過(guò),這里就不再贅述了。
接下來(lái)我們先來(lái)聊聊棧是如何垃圾回收的。其實(shí)棧的回收很簡(jiǎn)單,簡(jiǎn)單來(lái)說(shuō)就是一個(gè)函數(shù) push 進(jìn)棧,執(zhí)行完畢以后 pop 出來(lái)就當(dāng)可以回收了。當(dāng)然我們往深層了講深層了講就是匯編里的東西了,操作 esp 和 ebp 指針,了解下即可。
然后就是堆如何回收垃圾了,這部分的話(huà)會(huì)分為兩個(gè)空間及多個(gè)算法。
兩個(gè)空間分別為新生代和老生代,我們分開(kāi)來(lái)講每個(gè)空間中涉及到的算法。
新生代
新生代中的對(duì)象一般存活時(shí)間較短,空間也較小,使用 Scavenge GC 算法。
在新生代空間中,內(nèi)存空間分為兩部分,分別為 From 空間和 To 空間。在這兩個(gè)空間中,必定有一個(gè)空間是使用的,另一個(gè)空間是空閑的。新分配的對(duì)象會(huì)被放入 From 空間中,當(dāng) From 空間被占滿(mǎn)時(shí),新生代 GC 就會(huì)啟動(dòng)了。算法會(huì)檢查 From 空間中存活的對(duì)象并復(fù)制到 To 空間中,如果有失活的對(duì)象就會(huì)銷(xiāo)毀。當(dāng)復(fù)制完成后將 From 空間和 To 空間互換,這樣 GC 就結(jié)束了。
老生代
老生代中的對(duì)象一般存活時(shí)間較長(zhǎng)且數(shù)量也多,使用了兩個(gè)算法,分別是標(biāo)記清除和標(biāo)記壓縮算法。
在講算法前,先來(lái)說(shuō)下什么情況下對(duì)象會(huì)出現(xiàn)在老生代空間中:
- 新生代中的對(duì)象是否已經(jīng)經(jīng)歷過(guò)一次以上 Scavenge 算法,如果經(jīng)歷過(guò)的話(huà),會(huì)將對(duì)象從新生代空間移到老生代空間中。
- To 空間的對(duì)象占比大小超過(guò) 25 %。在這種情況下,為了不影響到內(nèi)存分配,會(huì)將對(duì)象從新生代空間移到老生代空間中。
老生代中的空間很復(fù)雜,有如下幾個(gè)空間
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不變的對(duì)象空間
NEW_SPACE, // 新生代用于 GC 復(fù)制算法的空間
OLD_SPACE, // 老生代常駐對(duì)象空間
CODE_SPACE, // 老生代代碼對(duì)象空間
MAP_SPACE, // 老生代 map 對(duì)象
LO_SPACE, // 老生代大空間對(duì)象
NEW_LO_SPACE, // 新生代大空間對(duì)象
FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
在老生代中,以下情況會(huì)先啟動(dòng)標(biāo)記清除算法:
- 某一個(gè)空間沒(méi)有分塊的時(shí)候
- 空間中被對(duì)象超過(guò)一定限制
- 空間不能保證新生代中的對(duì)象移動(dòng)到老生代中
在這個(gè)階段中,會(huì)遍歷堆中所有的對(duì)象,然后標(biāo)記活的對(duì)象,在標(biāo)記完成后,銷(xiāo)毀所有沒(méi)有被標(biāo)記的對(duì)象。在標(biāo)記大型對(duì)內(nèi)存時(shí),可能需要幾百毫秒才能完成一次標(biāo)記。這就會(huì)導(dǎo)致一些性能上的問(wèn)題。為了解決這個(gè)問(wèn)題,2011 年,V8 從 stop-the-world 標(biāo)記切換到增量標(biāo)志。在增量標(biāo)記期間,GC 將標(biāo)記工作分解為更小的模塊,可以讓 JS 應(yīng)用邏輯在模塊間隙執(zhí)行一會(huì),從而不至于讓?xiě)?yīng)用出現(xiàn)停頓情況。但在 2018 年,GC 技術(shù)又有了一個(gè)重大突破,這項(xiàng)技術(shù)名為并發(fā)標(biāo)記。該技術(shù)可以讓 GC 掃描和標(biāo)記對(duì)象時(shí),同時(shí)允許 JS 運(yùn)行,你可以點(diǎn)擊 該博客 詳細(xì)閱讀。
清除對(duì)象后會(huì)造成堆內(nèi)存出現(xiàn)碎片的情況,當(dāng)碎片超過(guò)一定限制后會(huì)啟動(dòng)壓縮算法。在壓縮過(guò)程中,將活的對(duì)象向一端移動(dòng),直到所有對(duì)象都移動(dòng)完成然后清理掉不需要的內(nèi)存。
其它考點(diǎn)
0.1 + 0.2 !== 0.3
因?yàn)?JS 采用 IEEE 754 雙精度版本(64位),并且只要采用 IEEE 754 的語(yǔ)言都有該問(wèn)題。
不止 0.1 + 0.2 存在問(wèn)題,0.7 + 0.1、0.2 + 0.4 同樣也存在問(wèn)題。
存在問(wèn)題的原因是浮點(diǎn)數(shù)用二進(jìn)制表示的時(shí)候是無(wú)窮的,因?yàn)榫鹊膯?wèn)題,兩個(gè)浮點(diǎn)數(shù)相加會(huì)造成截?cái)鄟G失精度,因此再轉(zhuǎn)換為十進(jìn)制就出了問(wèn)題。
解決的辦法可以通過(guò)以下代碼:
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;
};
核心就是計(jì)算出兩個(gè)浮點(diǎn)數(shù)最大的小數(shù)長(zhǎng)度,比如說(shuō) 0.1 + 0.22 的小數(shù)最大長(zhǎng)度為 2,然后兩數(shù)乘上 10 的 2次冪再相加得出數(shù)字 32,然后除以 10 的 2次冪即可得出正確答案 0.32。
手寫(xiě)題
防抖
你是否在日常開(kāi)發(fā)中遇到一個(gè)問(wèn)題,在滾動(dòng)事件中需要做個(gè)復(fù)雜計(jì)算或者實(shí)現(xiàn)一個(gè)按鈕的防二次點(diǎn)擊操作。
這些需求都可以通過(guò)函數(shù)防抖動(dòng)來(lái)實(shí)現(xiàn)。尤其是第一個(gè)需求,如果在頻繁的事件回調(diào)中做復(fù)雜計(jì)算,很有可能導(dǎo)致頁(yè)面卡頓,不如將多次計(jì)算合并為一次計(jì)算,只在一個(gè)精確點(diǎn)做操作。
PS:防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用。區(qū)別在于,假設(shè)一個(gè)用戶(hù)一直觸發(fā)這個(gè)函數(shù),且每次觸發(fā)函數(shù)的間隔小于閾值,防抖的情況下只會(huì)調(diào)用一次,而節(jié)流會(huì)每隔一定時(shí)間調(diào)用函數(shù)。
我們先來(lái)看一個(gè)袖珍版的防抖理解一下防抖的實(shí)現(xiàn):
// func是用戶(hù)傳入需要防抖的函數(shù)
// wait是等待時(shí)間
const debounce = (func, wait = 50) => {
// 緩存一個(gè)定時(shí)器id
let timer = 0
// 這里返回的函數(shù)是每次用戶(hù)實(shí)際調(diào)用的防抖函數(shù)
// 如果已經(jīng)設(shè)定過(guò)定時(shí)器了就清空上一次的定時(shí)器
// 開(kāi)始一個(gè)新的定時(shí)器,延遲執(zhí)行用戶(hù)傳入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不難看出如果用戶(hù)調(diào)用該函數(shù)的間隔小于 wait 的情況下,上一次的時(shí)間還未到就被清除了,并不會(huì)執(zhí)行函數(shù)
這是一個(gè)簡(jiǎn)單版的防抖,但是有缺陷,這個(gè)防抖只能在最后調(diào)用。一般的防抖會(huì)有immediate選項(xiàng),表示是否立即調(diào)用。這兩者的區(qū)別,舉個(gè)例子來(lái)說(shuō):
- 例如在搜索引擎搜索問(wèn)題的時(shí)候,我們當(dāng)然是希望用戶(hù)輸入完最后一個(gè)字才調(diào)用查詢(xún)接口,這個(gè)時(shí)候適用延遲執(zhí)行的防抖函數(shù),它總是在一連串(間隔小于wait的)函數(shù)觸發(fā)之后調(diào)用。
- 例如用戶(hù)給interviewMap點(diǎn)star的時(shí)候,我們希望用戶(hù)點(diǎn)第一下的時(shí)候就去調(diào)用接口,并且成功之后改變star按鈕的樣子,用戶(hù)就可以立馬得到反饋是否star成功了,這個(gè)情況適用立即執(zhí)行的防抖函數(shù),它總是在第一次調(diào)用,并且下一次調(diào)用必須與前一次調(diào)用的時(shí)間間隔大于wait才會(huì)觸發(fā)。
下面我們來(lái)實(shí)現(xiàn)一個(gè)帶有立即執(zhí)行選項(xiàng)的防抖函數(shù)
// 這個(gè)是用來(lái)獲取當(dāng)前時(shí)間戳的
function now() {
return +new Date()
}
/**
* 防抖函數(shù),返回函數(shù)連續(xù)調(diào)用時(shí),空閑時(shí)間必須大于或等于 wait,func 才會(huì)執(zhí)行
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時(shí)間窗口的間隔
* @param {boolean} immediate 設(shè)置為ture時(shí),是否立即調(diào)用函數(shù)
* @return {function} 返回客戶(hù)調(diào)用函數(shù)
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延遲執(zhí)行函數(shù)
const later = () => setTimeout(() => {
// 延遲函數(shù)執(zhí)行完畢,清空緩存的定時(shí)器序號(hào)
timer = null
// 延遲執(zhí)行的情況下,函數(shù)會(huì)在延遲函數(shù)中執(zhí)行
// 使用到之前緩存的參數(shù)和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這里返回的函數(shù)是每次實(shí)際調(diào)用的函數(shù)
return function(...params) {
// 如果沒(méi)有創(chuàng)建延遲執(zhí)行函數(shù)(later),就創(chuàng)建一個(gè)
if (!timer) {
timer = later()
// 如果是立即執(zhí)行,調(diào)用函數(shù)
// 否則緩存參數(shù)和調(diào)用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延遲執(zhí)行函數(shù)(later),調(diào)用的時(shí)候清除原來(lái)的并重新設(shè)定一個(gè)
// 這樣做延遲函數(shù)會(huì)重新計(jì)時(shí)
} else {
clearTimeout(timer)
timer = later()
}
}
}
整體函數(shù)實(shí)現(xiàn)的不難,總結(jié)一下。
- 對(duì)于按鈕防點(diǎn)擊來(lái)說(shuō)的實(shí)現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調(diào)用,如果函數(shù)是延遲執(zhí)行的,就緩存上下文和參數(shù),放到延遲函數(shù)中去執(zhí)行。一旦我開(kāi)始一個(gè)定時(shí)器,只要我定時(shí)器還在,你每次點(diǎn)擊我都重新計(jì)時(shí)。一旦你點(diǎn)累了,定時(shí)器時(shí)間到,定時(shí)器重置為 null,就可以再次點(diǎn)擊了。
- 對(duì)于延時(shí)執(zhí)行函數(shù)來(lái)說(shuō)的實(shí)現(xiàn):清除定時(shí)器ID,如果是延遲調(diào)用就調(diào)用函數(shù)
節(jié)流
防抖動(dòng)和節(jié)流本質(zhì)是不一樣的。防抖動(dòng)是將多次執(zhí)行變?yōu)樽詈笠淮螆?zhí)行,節(jié)流是將多次執(zhí)行變成每隔一段時(shí)間執(zhí)行。
/**
* underscore 節(jié)流函數(shù),返回函數(shù)連續(xù)調(diào)用時(shí),func 執(zhí)行頻率限定為 次 / wait
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時(shí)間窗口的間隔
* @param {object} options 如果想忽略開(kāi)始函數(shù)的的調(diào)用,傳入{leading: false}。
* 如果想忽略結(jié)尾函數(shù)的調(diào)用,傳入{trailing: false}
* 兩者不能共存,否則函數(shù)不能執(zhí)行
* @return {function} 返回客戶(hù)調(diào)用函數(shù)
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的時(shí)間戳
var previous = 0;
// 如果 options 沒(méi)傳則設(shè)為空對(duì)象
if (!options) options = {};
// 定時(shí)器回調(diào)函數(shù)
var later = function() {
// 如果設(shè)置了 leading,就將 previous 設(shè)為 0
// 用于下面函數(shù)的第一個(gè) if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是為了防止內(nèi)存泄漏,二是為了下面的定時(shí)器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 獲得當(dāng)前時(shí)間戳
var now = _.now();
// 首次進(jìn)入前者肯定為 true
// 如果需要第一次不執(zhí)行函數(shù)
// 就將上次時(shí)間戳設(shè)為當(dāng)前的
// 這樣在接下來(lái)計(jì)算 remaining 的值時(shí)會(huì)大于0
if (!previous && options.leading === false) previous = now;
// 計(jì)算剩余時(shí)間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當(dāng)前調(diào)用已經(jīng)大于上次調(diào)用時(shí)間 + wait
// 或者用戶(hù)手動(dòng)調(diào)了時(shí)間
// 如果設(shè)置了 trailing,只會(huì)進(jìn)入這個(gè)條件
// 如果沒(méi)有設(shè)置 leading,那么第一次會(huì)進(jìn)入這個(gè)條件
// 還有一點(diǎn),你可能會(huì)覺(jué)得開(kāi)啟了定時(shí)器那么應(yīng)該不會(huì)進(jìn)入這個(gè) if 條件了
// 其實(shí)還是會(huì)進(jìn)入的,因?yàn)槎〞r(shí)器的延時(shí)
// 并不是準(zhǔn)確的時(shí)間,很可能你設(shè)置了2秒
// 但是他需要2.2秒才觸發(fā),這時(shí)候就會(huì)進(jìn)入這個(gè)條件
if (remaining <= 0 || remaining > wait) {
// 如果存在定時(shí)器就清理掉否則會(huì)調(diào)用二次回調(diào)
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判斷是否設(shè)置了定時(shí)器和 trailing
// 沒(méi)有的話(huà)就開(kāi)啟一個(gè)定時(shí)器
// 并且不能不能同時(shí)設(shè)置 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(`沒(méi)有傳入回調(diào)函數(shù)`);
return
}
this.addEvent(key, fn, false, ...args)
}
fire(key, ...args) {
if (!this.events.get(key)) {
console.warn(`沒(méi)有 ${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 可以正確的判斷對(duì)象的類(lèi)型,因?yàn)閮?nèi)部機(jī)制是通過(guò)判斷對(duì)象的原型鏈中是不是能找到類(lèi)型的 prototype。
function instanceof(left, right) {
// 獲得類(lèi)型的原型
let prototype = right.prototype
// 獲得對(duì)象的原型
left = left.__proto__
// 判斷對(duì)象的類(lèi)型是否等于類(lèi)型的原型
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)
// 返回一個(gè)函數(shù)
return function F() {
// 因?yàn)榉祷亓艘粋€(gè)函數(shù),我們可以 new F(),所以需要判斷
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
其他
其他手寫(xiě)題上文已經(jīng)有提及,比如模擬 new、ES5 實(shí)現(xiàn)繼承、深拷貝。
另外大家可能經(jīng)常能看到手寫(xiě) Promise 的文章,其實(shí)根據(jù)筆者目前收集到的數(shù)百道面試題以及讀者的反饋來(lái)看,壓根就沒(méi)人遇到這個(gè)考點(diǎn),所以我們大可不必在這上面花時(shí)間。