日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線(xiàn)咨詢(xún)客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

閱讀前重要提示:

本文非百科全書(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)型:

  1. 原始類(lèi)型
  2. 對(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):

  1. 對(duì)象存儲(chǔ)的是引用地址,傳來(lái)傳去、賦值給別人那都是在傳遞值(存在棧上的那個(gè)內(nèi)容),別人一旦修改對(duì)象里的屬性,大家都被修改了。
  2. 但是一旦對(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ō)法。

17K star 倉(cāng)庫(kù),解決 90% 的大廠(chǎng)基礎(chǔ)面試題

 

上圖中畫(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í)行步驟可大概分為以下幾步:

  1. 新生成了一個(gè)對(duì)象
  2. 對(duì)象連接到構(gòu)造函數(shù)原型上,并綁定 this
  3. 執(zhí)行構(gòu)造函數(shù)代碼
  4. 返回新對(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ū)別:

  1. ES6 繼承的子類(lèi)需要調(diào)用 super() 才能拿到子類(lèi),ES5 的話(huà)是通過(guò) apply 這種綁定的方式
  2. 類(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):

  1. 請(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é)果。
  2. 如何實(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):

  1. 什么任務(wù)會(huì)被丟到 Microtask Queue 和 Task Queue 中?它們分別代表了什么?
  2. 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í)行順序如下所示:

  1. 執(zhí)行同步代碼
  2. 執(zhí)行完所有同步代碼后且執(zhí)行棧為空,判斷是否有微任務(wù)需要執(zhí)行
  3. 執(zhí)行所有微任務(wù)且微任務(wù)隊(duì)列為空
  4. 是否有必要渲染頁(yè)面
  5. 執(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');
  1. 遇到 console.log 執(zhí)行并打印
  2. 遇到 setTimeout,將回調(diào)加入宏任務(wù)隊(duì)列
  3. 遇到 Promise.resolve(),此時(shí)狀態(tài)已經(jīng)改變,因此將 then 回調(diào)加入微任務(wù)隊(duì)列
  4. 遇到 console.log 執(zhí)行并打印

此時(shí)同步任務(wù)全部執(zhí)行完畢,分別打印了 'script start' 以及 'script end',開(kāi)始判斷是否有微任務(wù)需要執(zhí)行。

  1. 微任務(wù)隊(duì)列存在任務(wù),開(kāi)始執(zhí)行 then 回調(diào)函數(shù)
  2. 遇到 queueMicrotask,將回到加入微任務(wù)隊(duì)列
  3. 遇到 console.log 執(zhí)行并打印
  4. 檢查發(fā)現(xiàn)微任務(wù)隊(duì)列存在任務(wù),執(zhí)行 queueMicrotask 回調(diào)
  5. 遇到 console.log 執(zhí)行并打印

此時(shí)發(fā)現(xiàn)微任務(wù)隊(duì)列已經(jīng)清空,判斷是否需要進(jìn)行 UI 渲染。

  1. 執(zhí)行宏任務(wù),開(kāi)始執(zhí)行 setTimeout 回調(diào)
  2. 遇到 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í)間。

分享到:
標(biāo)簽:倉(cāng)庫(kù) star
用戶(hù)無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定