17K star Github,解決 90% 的大廠前端基礎(chǔ)面試題

數(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)并行需求

Promise all 錯(cuò)誤處理

手寫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))

? }

}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市酸纲,隨后出現(xiàn)的幾起案子捣鲸,更是在濱河造成了極大的恐慌,老刑警劉巖闽坡,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件栽惶,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡疾嗅,警方通過查閱死者的電腦和手機(jī)外厂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來代承,“玉大人汁蝶,你說我怎么就攤上這事÷坫玻” “怎么了掖棉?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)膀估。 經(jīng)常有香客問我幔亥,道長(zhǎng),這世上最難降的妖魔是什么察纯? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任帕棉,我火速辦了婚禮,結(jié)果婚禮上饼记,老公的妹妹穿的比我還像新娘香伴。我一直安慰自己,他們只是感情好具则,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布瞒窒。 她就那樣靜靜地躺著,像睡著了一般乡洼。 火紅的嫁衣襯著肌膚如雪崇裁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天束昵,我揣著相機(jī)與錄音拔稳,去河邊找鬼。 笑死锹雏,一個(gè)胖子當(dāng)著我的面吹牛巴比,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼轻绞,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼采记!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起政勃,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤唧龄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奸远,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體既棺,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年懒叛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丸冕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡薛窥,死狀恐怖胖烛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情诅迷,我是刑警寧澤洪己,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站竟贯,受9級(jí)特大地震影響答捕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屑那,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一拱镐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧持际,春花似錦沃琅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至姥份,卻和暖如春郭脂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澈歉。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工展鸡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人埃难。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓莹弊,卻偏偏與公主長(zhǎng)得像涤久,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忍弛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1响迂、什么是JavaScript? 基于對(duì)象和事件驅(qū)動(dòng)可解釋性腳本語言 2细疚、JavaScript與ECMAScrip...
    編碼夢(mèng)想家閱讀 439評(píng)論 0 0
  • 1. 說一下對(duì)HTML語義化的理解? 語義化就是選擇與語義相符合的標(biāo)簽蔗彤,使代碼語義化,這樣不僅便于開發(fā)者進(jìn)行閱讀惠昔,...
    陳二狗想吃肉閱讀 2,128評(píng)論 3 16
  • 本篇收錄了一些面試中經(jīng)常會(huì)遇到的經(jīng)典面試題以及自己面試過程中遇到的一些問題幕与,并且都給出了我在網(wǎng)上收集的答案挑势。馬上就...
    菲菲菲菲妞閱讀 912評(píng)論 0 3
  • client镇防,page和screen的區(qū)別? clientX潮饱,clientY是觸摸點(diǎn)相對(duì)于viewport視口x,...
    change_22fa閱讀 1,637評(píng)論 1 1
  • 一来氧、CSS問題 1.flex布局 display:flex; 在父元素設(shè)置,子元素受彈性盒影響香拉,默認(rèn)排成一行啦扬,如果...
    陳二狗想吃肉閱讀 551評(píng)論 0 9