數(shù)據(jù)類型
JS 數(shù)據(jù)類型分為兩大類蛾扇,九個(gè)數(shù)據(jù)類型:
原始類型
對(duì)象類型
其中原始類型又分為七種類型违寿,分別為:
boolean
number
string
undefined
null
symbol
bigint
對(duì)象類型分為兩種,分別為:
Object
Function
其中Object中又包含了很多子類型扫倡,比如Array谦秧、RegExp、Math镊辕、Map油够、Set等等,也就不一一列出了征懈。
原始類型存儲(chǔ)在棧上石咬,對(duì)象類型存儲(chǔ)在堆上,但是它的引用地址還是存在棧上卖哎。
注意:以上結(jié)論前半句是不準(zhǔn)確的鬼悠,更準(zhǔn)確的內(nèi)容我會(huì)在閉包章節(jié)里說明。
常見考點(diǎn)
JS 類型有哪些亏娜?
大數(shù)相加焕窝、相乘算法題,可以直接使用bigint维贺,當(dāng)然再加上字符串的處理會(huì)更好它掂。
NaN如何判斷
另外還有一類常見的題目是對(duì)于對(duì)象的修改,比如說往函數(shù)里傳一個(gè)對(duì)象進(jìn)去溯泣,函數(shù)內(nèi)部修改參數(shù)虐秋。
function test(person) {
? person.age = 26
? person = {}
? return person
}
const p1 = {
? age: 25
}
這類題目我們只需要牢記以下幾點(diǎn):
對(duì)象存儲(chǔ)的是引用地址,傳來傳去垃沦、賦值給別人那都是在傳遞值(存在棧上的那個(gè)內(nèi)容)客给,別人一旦修改對(duì)象里的屬性,大家都被修改了肢簿。
但是一旦對(duì)象被重新賦值了靶剑,只要不是原對(duì)象被重新賦值,那么就永遠(yuǎn)不會(huì)修改原對(duì)象池充。
類型判斷
類型判斷有好幾種方式桩引。
typeof
原始類型中除了null,其它類型都可以通過typeof來判斷收夸。
typeof null的值為object坑匠,這是因?yàn)橐粋€(gè)久遠(yuǎn)的 Bug,沒有細(xì)究的必要咱圆,了解即可笛辟。如果想具體判斷null類型的話直接xxx === null即可。
對(duì)于對(duì)象類型來說序苏,typeof只能具體判斷函數(shù)的類型為function手幢,其它均為object。
instanceof
instanceof內(nèi)部通過原型鏈的方式來判斷是否為構(gòu)建函數(shù)的實(shí)例忱详,常用于判斷具體的對(duì)象類型围来。
[] instanceof Array
都說instanceof只能判斷對(duì)象類型,其實(shí)這個(gè)說法是不準(zhǔn)確的匈睁,我們是可以通過 hake 的方式得以實(shí)現(xiàn)监透,雖然不會(huì)有人這樣去玩吧。
class CheckIsNumber {
? static [Symbol.hasInstance](number) {
? ? return typeof number === 'number'
? }
}
// true
1 instanceof CheckIsNumber
另外其實(shí)我們還可以直接通過構(gòu)建函數(shù)來判斷類型:
[].constructor === Array
Object.prototype.toString
前幾種方式或多或少都存在一些缺陷航唆,Object.prototype.toString綜合來看是最佳選擇胀蛮,能判斷的類型最完整。
上圖是一部分類型判斷糯钙,更多的就不列舉了粪狼,[object XXX]中的XXX就是判斷出來的類型。
isXXX API
同時(shí)還存在一些判斷特定類型的 API任岸,選了兩個(gè)常見的:
常見考點(diǎn)
JS 類型如何判斷再榄,有哪幾種方式可用
instanceof原理
手寫instanceof
類型轉(zhuǎn)換
類型轉(zhuǎn)換分為兩種情況,分別為強(qiáng)制轉(zhuǎn)換及隱式轉(zhuǎn)換享潜。
強(qiáng)制轉(zhuǎn)換
強(qiáng)制轉(zhuǎn)換就是轉(zhuǎn)成特定的類型:
Number(false) // -> 0
Number('1') // -> 1
Number('zb') // -> NaN
(1).toString() // '1'
這部分是日常常用的內(nèi)容困鸥,就不具體展開說了,主要記住強(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ù)筆者目前收集到的最新面試題來說薄坏,這部分考題基本絕跡了,當(dāng)然講還是講一下吧寨闹。
對(duì)象轉(zhuǎn)基本類型:
調(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í)沈善,其中一方是字符串類型,就會(huì)把另一個(gè)也轉(zhuǎn)為字符串類型
其他運(yùn)算只要其中一方是數(shù)字椭蹄,那么另一方就轉(zhuǎn)為數(shù)字
==操作符
常見考點(diǎn)
如果這部分規(guī)則記不住也不礙事闻牡,確實(shí)有點(diǎn)繁瑣,而且考的也越來越少了绳矩,拿一道以前痴秩螅考的題目看看吧:
[] == ![] // -> ?
this
this是很多人會(huì)混淆的概念,但是其實(shí)他一點(diǎn)都不難埋酬,不要被那些長(zhǎng)篇大論的文章嚇住了(我其實(shí)也不知道為什么他們能寫那么多字)哨啃,你只需要記住幾個(gè)規(guī)則就可以了。
普通函數(shù)
function foo() {
console.log(this.a)
}
var a = 1
foo()
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上情況就是看函數(shù)是被誰調(diào)用写妥,那么 `this` 就是誰拳球,沒有被對(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ù)沒有this,所以一切妄圖改變箭頭函數(shù)this指向都是無效的嗜桌。
箭頭函數(shù)的this只取決于定義時(shí)的環(huán)境奥溺。比如如下代碼中的fn箭頭函數(shù)是在windows環(huán)境下定義的,無論如何調(diào)用骨宠,this都指向window浮定。
var a = 1
const fn = () => {
? console.log(this.a)
}
const obj = {
? fn,
? a: 2
}
obj.fn()
常見考點(diǎn)
這里一般都是考this的指向問題,牢記上述的幾個(gè)規(guī)則就夠用了层亿,比如下面這道題:
const a = {
? b: 2,
? foo: function () { console.log(this.b) }
}
function b(foo) {
? // 輸出什么桦卒?
? foo()
}
b(a.foo)
閉包
首先閉包正確的定義是:假如一個(gè)函數(shù)能訪問外部的變量,那么這個(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è)作用是訪問私有變量裕偿,就比如上述代碼中的fn2訪問到了fn1函數(shù)中的變量a洞慎。但是此時(shí)fn1早已銷毀,我們是如何訪問到變量a的呢嘿棘?不是都說原始類型是存放在棧上的么拢蛋,為什么此時(shí)卻沒有被銷毀掉?
接下來筆者會(huì)根據(jù)瀏覽器的表現(xiàn)來重新理解關(guān)于原始類型存放位置的說法蔫巩。
先來說下數(shù)據(jù)存放的正確規(guī)則是:局部、占用空間確定的數(shù)據(jù)快压,一般會(huì)存放在棧中圆仔,否則就在堆中(也有例外)。 那么接下來我們可以通過 Chrome 來幫助我們驗(yàn)證這個(gè)說法說法蔫劣。
上圖中畫紅框的位置我們能看到一個(gè)內(nèi)部的對(duì)象[[Scopes]]坪郭,其中存放著變量a,該對(duì)象是被存放在堆上的脉幢,其中包含了閉包歪沃、全局對(duì)象等等內(nèi)容,因此我們能通過閉包訪問到本該銷毀的變量嫌松。
另外最開始我們對(duì)于閉包的定位是:假如一個(gè)函數(shù)能訪問外部的變量沪曙,那么這個(gè)函數(shù)它就是一個(gè)閉包,因此接下來我們看看在全局下的表現(xiàn)是怎么樣的萎羔。
let a = 1
var b = 2
// fn 是閉包
function fn() {
? console.log(a, b);
}
從上圖我們能發(fā)現(xiàn)全局下聲明的變量液走,如果是 var 的話就直接被掛到globe上,如果是其他關(guān)鍵字聲明的話就被掛到Script上贾陷。雖然這些內(nèi)容同樣還是存在[[Scopes]]缘眶,但是全局變量應(yīng)該是存放在靜態(tài)區(qū)域的,因?yàn)槿肿兞繜o需進(jìn)行垃圾回收髓废,等需要回收的時(shí)候整個(gè)應(yīng)用都沒了巷懈。
只有在下圖的場(chǎng)景中,原始類型才可能是被存儲(chǔ)在棧上慌洪。
這里為什么要說可能顶燕,是因?yàn)?JS 是門動(dòng)態(tài)類型語言,一個(gè)變量聲明時(shí)可以是原始類型蒋譬,馬上又可以賦值為對(duì)象類型割岛,然后又回到原始類型。這樣頻繁的在堆棧上切換存儲(chǔ)位置犯助,內(nèi)部引擎是不是也會(huì)有什么優(yōu)化手段癣漆,或者干脆全部都丟堆上?只有const聲明的原始類型才一定存在棧上剂买?當(dāng)然這只是筆者的一個(gè)推測(cè)惠爽,暫時(shí)沒有深究癌蓖,讀者可以忽略這段瞎想。
因此筆者對(duì)于原始類型存儲(chǔ)位置的理解為:局部變量才是被存儲(chǔ)在棧上婚肆,全局變量存在靜態(tài)區(qū)域上租副,其它都存儲(chǔ)在堆上。
當(dāng)然這個(gè)理解是建立的 Chrome 的表現(xiàn)之上的较性,在不同的瀏覽器上因?yàn)橐娴牟煌蒙赡艽鎯?chǔ)的方式還是有所變化的。
常見考點(diǎn)
閉包能考的很多赞咙,概念和筆試題都會(huì)考责循。
概念題就是考考閉包是什么了。
筆試題的話基本都會(huì)結(jié)合上異步攀操,比如最常見的:
for (var i = 0; i < 6; i++) {
? setTimeout(() => {
? ? console.log(i)
? })
}
這道題會(huì)問輸出什么院仿,有哪幾種方式可以得到想要的答案?
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)建出來的新對(duì)象就被我們返回的對(duì)象所覆蓋排惨,所以一般來說構(gòu)建函數(shù)就別返回對(duì)象了(返回原始類型不影響)。
常見考點(diǎn)
new做了那些事碰凶?
new返回不同的類型時(shí)會(huì)有什么表現(xiàn)若贮?
手寫new的實(shí)現(xiàn)過程
作用域
作用域可以理解為變量的可訪問性,總共分為三種類型痒留,分別為:
全局作用域
函數(shù)作用域
塊級(jí)作用域谴麦,ES6 中的let、const就可以產(chǎn)生該作用域
其實(shí)看完前面的閉包伸头、this這部分內(nèi)部的話匾效,應(yīng)該基本能了解作用域的一些應(yīng)用。
一旦我們將這些作用域嵌套起來恤磷,就變成了另外一個(gè)重要的知識(shí)點(diǎn)「作用域鏈」面哼,也就是 JS 到底是如何訪問需要的變量或者函數(shù)的。
首先作用域鏈?zhǔn)窃诙x時(shí)就被確定下來的扫步,和箭頭函數(shù)里的this一樣魔策,后續(xù)不會(huì)改變,JS 會(huì)一層層往上尋找需要的內(nèi)容河胎。
其實(shí)作用域鏈這個(gè)東西我們?cè)陂]包小結(jié)中已經(jīng)看到過它的實(shí)體了:[[Scopes]]
圖中的[[Scopes]]是個(gè)數(shù)組闯袒,作用域的一層層往上尋找就等同于遍歷[[Scopes]]。
常見考點(diǎn)
什么是作用域
什么是作用域鏈
原型
原型在面試?yán)镏恍枰獛拙湓挕⒁粡垐D的概念就夠用了政敢,沒人會(huì)讓你長(zhǎng)篇大論講上一堆內(nèi)容的其徙,問原型更多的是為了引出繼承這個(gè)話題。
根據(jù)上圖喷户,原型總結(jié)下來的概念為:
所有對(duì)象都有一個(gè)屬性__proto__指向一個(gè)對(duì)象唾那,也就是原型
每個(gè)對(duì)象的原型都可以通過constructor找到構(gòu)造函數(shù),構(gòu)造函數(shù)也可以通過prototype找到原型
所有函數(shù)都可以通過__proto__找到Function對(duì)象
所有對(duì)象都可以通過__proto__找到Object對(duì)象
對(duì)象之間通過__proto__連接起來褪尝,這樣稱之為原型鏈闹获。當(dāng)前對(duì)象上不存在的屬性可以通過原型鏈一層層往上查找,直到頂層Object對(duì)象河哑,再往上就是null了
常見考點(diǎn)
聊聊你理解的原型是什么
繼承
即使是 ES6 中的class也不是其他語言里的類昌罩,本質(zhì)就是一個(gè)函數(shù)。
class Person {}
Person instanceof Function // true
其實(shí)在當(dāng)下都用 ES6 的情況下灾馒,ES5 的繼承寫法已經(jīng)沒啥學(xué)習(xí)的必要了,但是因?yàn)槊嬖囘€會(huì)被問到遣总,所以復(fù)習(xí)一下還是需要的睬罗。
首先來說下 ES5 和 6 繼承的區(qū)別:
ES6 繼承的子類需要調(diào)用super()才能拿到子類,ES5 的話是通過apply這種綁定的方式
類聲明不會(huì)提升旭斥,和let這些一致
接下來就是回字的幾種寫法的名場(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()
常見考點(diǎn)
JS 中如何實(shí)現(xiàn)繼承
通過原型實(shí)現(xiàn)的繼承和class有何區(qū)別
手寫任意一種原型繼承
深淺拷貝
淺拷貝
兩個(gè)對(duì)象第一層的引用不相同就是淺拷貝的含義垂券。
我們可以通過assign花盐、擴(kuò)展運(yùn)算符等方式來實(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))菇爪,但是該方法存在不少缺陷算芯。
比如說只支持 JSON 支持的類型,JSON 是門通用的語言凳宙,并不支持 JS 中的所有類型熙揍。
同時(shí)還存在不能處理循環(huán)引用的問題:
如果想解決以上問題,我們可以通過遞歸的方式來實(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ì)象出來
? ? 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
}
上述代碼解決了常見的類型以及循環(huán)引用的問題氏涩,當(dāng)然還是一部分缺陷的届囚,但是面試時(shí)候能寫出上面的代碼已經(jīng)足夠了,剩下的能口述思路基本這道題就能拿到高分了是尖。
比如說遞歸肯定會(huì)存在爆棧的問題意系,因?yàn)閳?zhí)行棧的大小是有限制的,到一定數(shù)量棧就會(huì)爆掉饺汹。
因此遇到這種問題蛔添,我們可以通過遍歷的方式來改寫遞歸。這個(gè)就是如何寫層序遍歷(BFS)的問題了,通過數(shù)組來模擬執(zhí)行棧就能解決爆棧問題作郭,有興趣的讀者可以咨詢查閱陨囊。
Promise
Promise是一個(gè)高頻考點(diǎn)了,但是更多的是在筆試題中出現(xiàn)夹攒,概念題反倒基本沒有蜘醋,多是來問 Event loop 的。
對(duì)于這塊內(nèi)容的復(fù)習(xí)我們需要熟悉涉及到的所有 API咏尝,因?yàn)榭碱}里可能會(huì)問到all压语、race等等用法或者需要你用這些 API 實(shí)現(xiàn)一些功能。
對(duì)于Promise進(jìn)階點(diǎn)的知識(shí)可以具體閱讀筆者的這篇文章编检,這里就不復(fù)制過來占用篇幅了:Promise 你真的用明白了么胎食?
常見考點(diǎn)
使用all實(shí)現(xiàn)并行需求
手寫all的實(shí)現(xiàn)
另外還有一道很常見的串行題目:
頁(yè)面上有三個(gè)按鈕,分別為 A允懂、B厕怜、C,點(diǎn)擊各個(gè)按鈕都會(huì)發(fā)送異步請(qǐng)求且互不影響蕾总,每次請(qǐng)求回來的數(shù)據(jù)都為按鈕的名字粥航。 請(qǐng)實(shí)現(xiàn)當(dāng)用戶依次點(diǎn)擊 A、B生百、C递雀、A、C蚀浆、B 的時(shí)候缀程,最終獲取的數(shù)據(jù)為 ABCACB。
這道題目主要兩個(gè)考點(diǎn):
請(qǐng)求不能阻塞市俊,但是輸出可以阻塞杨凑。比如說 B 請(qǐng)求需要耗時(shí) 3 秒,其他請(qǐng)求耗時(shí) 1 秒摆昧,那么當(dāng)用戶點(diǎn)擊 BAC 時(shí)蠢甲,三個(gè)請(qǐng)求都應(yīng)該發(fā)起,但是因?yàn)?B 請(qǐng)求回來的慢据忘,所以得等著輸出結(jié)果鹦牛。
如何實(shí)現(xiàn)一個(gè)隊(duì)列?
其實(shí)我們無需自己去構(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ì)問到和promise的一些區(qū)別驹吮。
await相比直接使用Promise來說,優(yōu)勢(shì)在于處理then的調(diào)用鏈晶伦,能夠更清晰準(zhǔn)確的寫出代碼碟狞。缺點(diǎn)在于濫用await可能會(huì)導(dǎo)致性能問題,因?yàn)閍wait會(huì)阻塞代碼婚陪,也許之后的異步代碼并不依賴于前者族沃,但仍然需要等待前者完成,導(dǎo)致代碼失去了并發(fā)性泌参,此時(shí)更應(yīng)該使用Promise.all脆淹。
下面來看一道很容易做錯(cuò)的筆試題。
vara =0varb =async() => {? a = a +await10console.log('2', a)// -> 沽一?}b()a++console.log('1', a)// -> 盖溺?復(fù)制代碼
這道題目大部分讀者肯定會(huì)想到await左邊是異步代碼,因此會(huì)先把同步代碼執(zhí)行完铣缠,此時(shí)a已經(jīng)變成 1烘嘱,所以答案應(yīng)該是 11。
其實(shí)a為 0 是因?yàn)榧臃ㄟ\(yùn)算法蝗蛙,先算左邊再算右邊蝇庭,所以會(huì)把 0 固定下來。如果我們把題目改成await 10 + a的話歼郭,答案就是 11 了。
事件循環(huán)
在開始講事件循環(huán)之前辐棒,我們一定要牢記一點(diǎn):JS 是一門單線程語言病曾,在執(zhí)行過程中永遠(yuǎn)只能同時(shí)執(zhí)行一個(gè)任務(wù),任何異步的調(diào)用都只是在模擬這個(gè)過程漾根,或者說可以直接認(rèn)為在 JS 中的異步就是延遲執(zhí)行的同步代碼泰涂。另外別的什么 Web worker、瀏覽器提供的各種線程都不會(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ù)會(huì)被丟到 Microtask Queue 和 Task Queue 中陕截?它們分別代表了什么驳棱?
Event loop 是如何處理這些 task 的?
首先我們來解決問題一农曲。
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)沒變,回調(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í)行了互亮。
接下來我們來解決問題二。
Event Loop 執(zhí)行順序如下所示:
執(zhí)行同步代碼
執(zhí)行完所有同步代碼后且執(zhí)行棧為空余素,判斷是否有微任務(wù)需要執(zhí)行
執(zhí)行所有微任務(wù)且微任務(wù)隊(duì)列為空
是否有必要渲染頁(yè)面
執(zhí)行一個(gè)宏任務(wù)
如果你覺得上面的表述不大理解的話豹休,接下來我們通過代碼示例來鞏固理解上面的知識(shí):
cconsole.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'视乐,開始判斷是否有微任務(wù)需要執(zhí)行洛搀。
微任務(wù)隊(duì)列存在任務(wù),開始執(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ù)谎砾,開始執(zhí)行setTimeout回調(diào)
遇到console.log執(zhí)行并打印
執(zhí)行一個(gè)宏任務(wù)即結(jié)束抚太,尋找是否存在微任務(wù)山上,開始循環(huán)判斷...
其實(shí)事件循環(huán)沒啥難懂的,理解 JS 是個(gè)單線程語言麻献,明白哪些是微宏任務(wù)隘道、循環(huán)的順序就好了症歇。
最后需要注意的一點(diǎn):正是因?yàn)?JS 是門單線程語言郎笆,只能同時(shí)執(zhí)行一個(gè)任務(wù)。因此所有的任務(wù)都可能因?yàn)橹叭蝿?wù)的執(zhí)行時(shí)間過長(zhǎng)而被延遲執(zhí)行忘晤,尤其對(duì)于一些定時(shí)器而言宛蚓。
常見考點(diǎn)
什么是事件循環(huán)?
JS 的執(zhí)行原理设塔?
哪些是微宏任務(wù)凄吏?
定時(shí)器是準(zhǔn)時(shí)的嘛?
模塊化
當(dāng)下模塊化主要就是 CommonJS 和 ES6 的 ESM 了闰蛔,其它什么的 AMD痕钢、UMD 了解下就行了。
ESM 我想應(yīng)該沒啥好說的了序六,主要我們來聊聊 CommonJS 以及 ESM 和 CommonJS 的區(qū)別任连。
CommonJS
CommonJs 是 Node 獨(dú)有的規(guī)范,當(dāng)然 Webpack 也自己實(shí)現(xiàn)了這套東西例诀,讓我們能在瀏覽器里跑起來這個(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很容易混淆繁涂,讓我們來看看大致內(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)入即使卡住主線程影響也不大。而后者是異步導(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)講過胧华,這里就不再贅述了寄症。
接下來我們先來聊聊棧是如何垃圾回收的。其實(shí)棧的回收很簡(jiǎn)單矩动,簡(jiǎn)單來說就是一個(gè)函數(shù) push 進(jìn)棧有巧,執(zhí)行完畢以后 pop 出來就當(dāng)可以回收了。當(dāng)然我們往深層了講深層了講就是匯編里的東西了悲没,操作 esp 和 ebp 指針篮迎,了解下即可。
然后就是堆如何回收垃圾了示姿,這部分的話會(huì)分為兩個(gè)空間及多個(gè)算法甜橱。
兩個(gè)空間分別為新生代和老生代,我們分開來講每個(gè)空間中涉及到的算法栈戳。
新生代
新生代中的對(duì)象一般存活時(shí)間較短岂傲,空間也較小,使用 Scavenge GC 算法荧琼。
在新生代空間中譬胎,內(nèi)存空間分為兩部分,分別為 From 空間和 To 空間命锄。在這兩個(gè)空間中堰乔,必定有一個(gè)空間是使用的,另一個(gè)空間是空閑的脐恩。新分配的對(duì)象會(huì)被放入 From 空間中镐侯,當(dāng) From 空間被占滿時(shí),新生代 GC 就會(huì)啟動(dòng)了驶冒。算法會(huì)檢查 From 空間中存活的對(duì)象并復(fù)制到 To 空間中苟翻,如果有失活的對(duì)象就會(huì)銷毀。當(dāng)復(fù)制完成后將 From 空間和 To 空間互換骗污,這樣 GC 就結(jié)束了崇猫。
老生代
老生代中的對(duì)象一般存活時(shí)間較長(zhǎng)且數(shù)量也多,使用了兩個(gè)算法需忿,分別是標(biāo)記清除和標(biāo)記壓縮算法诅炉。
在講算法前,先來說下什么情況下對(duì)象會(huì)出現(xiàn)在老生代空間中:
新生代中的對(duì)象是否已經(jīng)經(jīng)歷過一次以上 Scavenge 算法屋厘,如果經(jīng)歷過的話涕烧,會(huì)將對(duì)象從新生代空間移到老生代空間中。
To 空間的對(duì)象占比大小超過 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è)空間沒有分塊的時(shí)候
空間中被對(duì)象超過一定限制
空間不能保證新生代中的對(duì)象移動(dòng)到老生代中
在這個(gè)階段中,會(huì)遍歷堆中所有的對(duì)象阀参,然后標(biāo)記活的對(duì)象浓恶,在標(biāo)記完成后,銷毀所有沒有被標(biāo)記的對(duì)象结笨。在標(biāo)記大型對(duì)內(nèi)存時(shí)包晰,可能需要幾百毫秒才能完成一次標(biāo)記。這就會(huì)導(dǎo)致一些性能上的問題炕吸。為了解決這個(gè)問題伐憾,2011 年,V8 從 stop-the-world 標(biāo)記切換到增量標(biāo)志赫模。在增量標(biāo)記期間树肃,GC 將標(biāo)記工作分解為更小的模塊,可以讓 JS 應(yīng)用邏輯在模塊間隙執(zhí)行一會(huì)瀑罗,從而不至于讓應(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)碎片超過一定限制后會(huì)啟動(dòng)壓縮算法诬像。在壓縮過程中屋群,將活的對(duì)象像一端移動(dòng),直到所有對(duì)象都移動(dòng)完成然后清理掉不需要的內(nèi)存坏挠。
其它考點(diǎn)
0.1 + 0.2 !== 0.3
因?yàn)?JS 采用 IEEE 754 雙精度版本(64位)芍躏,并且只要采用 IEEE 754 的語言都有該問題。
不止 0.1 + 0.2 存在問題降狠,0.7 + 0.1对竣、0.2 + 0.4 同樣也存在問題。
存在問題的原因是浮點(diǎn)數(shù)用二進(jìn)制表示的時(shí)候是無窮的喊熟,因?yàn)榫鹊膯栴}柏肪,兩個(gè)浮點(diǎn)數(shù)相加會(huì)造成截?cái)鄟G失精度姐刁,因此再轉(zhuǎn)換為十進(jìn)制就出了問題芥牌。
解決的辦法可以通過以下代碼:
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)度,比如說 0.1 + 0.22 的小數(shù)最大長(zhǎng)度為 2聂使,然后兩數(shù)乘上 10 的 2次冪再相加得出數(shù)字 32壁拉,然后除以 10 的 2次冪即可得出正確答案 0.32谬俄。
手寫題
防抖
你是否在日常開發(fā)中遇到一個(gè)問題,在滾動(dòng)事件中需要做個(gè)復(fù)雜計(jì)算或者實(shí)現(xiàn)一個(gè)按鈕的防二次點(diǎn)擊操作弃理。
這些需求都可以通過函數(shù)防抖動(dòng)來實(shí)現(xiàn)溃论。尤其是第一個(gè)需求,如果在頻繁的事件回調(diào)中做復(fù)雜計(jì)算痘昌,很有可能導(dǎo)致頁(yè)面卡頓钥勋,不如將多次計(jì)算合并為一次計(jì)算,只在一個(gè)精確點(diǎn)做操作辆苔。
PS:防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用算灸。區(qū)別在于,假設(shè)一個(gè)用戶一直觸發(fā)這個(gè)函數(shù)驻啤,且每次觸發(fā)函數(shù)的間隔小于閾值菲驴,防抖的情況下只會(huì)調(diào)用一次,而節(jié)流會(huì)每隔一定時(shí)間調(diào)用函數(shù)骑冗。
我們先來看一個(gè)袖珍版的防抖理解一下防抖的實(shí)現(xiàn):
// func是用戶傳入需要防抖的函數(shù)
// wait是等待時(shí)間
const debounce = (func, wait = 50) => {
? // 緩存一個(gè)定時(shí)器id
? let timer = 0
? // 這里返回的函數(shù)是每次用戶實(shí)際調(diào)用的防抖函數(shù)
? // 如果已經(jīng)設(shè)定過定時(shí)器了就清空上一次的定時(shí)器
? // 開始一個(gè)新的定時(shí)器赊瞬,延遲執(zhí)行用戶傳入的方法
? return function(...args) {
? ? if (timer) clearTimeout(timer)
? ? timer = setTimeout(() => {
? ? ? func.apply(this, args)
? ? }, wait)
? }
}
// 不難看出如果用戶調(diào)用該函數(shù)的間隔小于 wait 的情況下,上一次的時(shí)間還未到就被清除了贼涩,并不會(huì)執(zhí)行函數(shù)
這是一個(gè)簡(jiǎn)單版的防抖巧涧,但是有缺陷,這個(gè)防抖只能在最后調(diào)用遥倦。一般的防抖會(huì)有immediate選項(xiàng)褒侧,表示是否立即調(diào)用。這兩者的區(qū)別谊迄,舉個(gè)栗子來說:
例如在搜索引擎搜索問題的時(shí)候闷供,我們當(dāng)然是希望用戶輸入完最后一個(gè)字才調(diào)用查詢接口,這個(gè)時(shí)候適用延遲執(zhí)行的防抖函數(shù)统诺,它總是在一連串(間隔小于wait的)函數(shù)觸發(fā)之后調(diào)用歪脏。
例如用戶給interviewMap點(diǎn)star的時(shí)候,我們希望用戶點(diǎn)第一下的時(shí)候就去調(diào)用接口粮呢,并且成功之后改變star按鈕的樣子婿失,用戶就可以立馬得到反饋是否star成功了,這個(gè)情況適用立即執(zhí)行的防抖函數(shù)啄寡,它總是在第一次調(diào)用豪硅,并且下一次調(diào)用必須與前一次調(diào)用的時(shí)間間隔大于wait才會(huì)觸發(fā)。
下面我們來實(shí)現(xiàn)一個(gè)帶有立即執(zhí)行選項(xiàng)的防抖函數(shù)
// 這個(gè)是用來獲取當(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}? ? ? ? ? ? 返回客戶調(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) {
? ? // 如果沒有創(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í)候清除原來的并重新設(shè)定一個(gè)
? ? // 這樣做延遲函數(shù)會(huì)重新計(jì)時(shí)
? ? } else {
? ? ? clearTimeout(timer)
? ? ? timer = later()
? ? }
? }
}
整體函數(shù)實(shí)現(xiàn)的不難,總結(jié)一下舌镶。
對(duì)于按鈕防點(diǎn)擊來說的實(shí)現(xiàn):如果函數(shù)是立即執(zhí)行的柱彻,就立即調(diào)用,如果函數(shù)是延遲執(zhí)行的餐胀,就緩存上下文和參數(shù)绒疗,放到延遲函數(shù)中去執(zhí)行。一旦我開始一個(gè)定時(shí)器骂澄,只要我定時(shí)器還在吓蘑,你每次點(diǎn)擊我都重新計(jì)時(shí)。一旦你點(diǎn)累了坟冲,定時(shí)器時(shí)間到磨镶,定時(shí)器重置為null,就可以再次點(diǎn)擊了健提。
對(duì)于延時(shí)執(zhí)行函數(shù)來說的實(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? 如果想忽略開始函數(shù)的的調(diào)用账千,傳入{leading: false}。
*? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 如果想忽略結(jié)尾函數(shù)的調(diào)用暗膜,傳入{trailing: false}
*? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 兩者不能共存匀奏,否則函數(shù)不能執(zhí)行
* @return {function}? ? ? ? ? ? 返回客戶調(diào)用函數(shù)?
*/
_.throttle = function(func, wait, options) {
? ? var context, args, result;
? ? var timeout = null;
? ? // 之前的時(shí)間戳
? ? var previous = 0;
? ? // 如果 options 沒傳則設(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)前的
? ? ? // 這樣在接下來計(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
? ? ? // 或者用戶手動(dòng)調(diào)了時(shí)間
? // 如果設(shè)置了 trailing娃善,只會(huì)進(jìn)入這個(gè)條件
? // 如果沒有設(shè)置 leading,那么第一次會(huì)進(jìn)入這個(gè)條件
? // 還有一點(diǎn)瑞佩,你可能會(huì)覺得開啟了定時(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
? ? // 沒有的話就開啟一個(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(`沒有傳入回調(diào)函數(shù)`);
? ? ? 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可以正確的判斷對(duì)象的類型瘫寝,因?yàn)閮?nèi)部機(jī)制是通過判斷對(duì)象的原型鏈中是不是能找到類型的prototype。
function instanceof(left, right) {
? ? // 獲得類型的原型
? ? let prototype = right.prototype
? ? // 獲得對(duì)象的原型
? ? left = left.__proto__
? ? // 判斷對(duì)象的類型是否等于類型的原型
? ? 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))
? }
}