JavaScript面試題詳解(基礎+進階)

本篇內容包括: JS 基礎1、二,ES6,JS進階,異步編程 等等,這些知識是我在一本書上看到的,書的名字我也忘了,並不是我本身的原創。我只是整理了一下。並不是我不想加這個書名,我真的忘了。。。


JS 基礎知識點及常考面試題(一)

JS 對於每位前端開發都是必備技能,在小冊中咱們也會有多個章節去講述這部分的知識。首先咱們先來熟悉下 JS 的一些常考和容易混亂的基礎知識點。html

原始(Primitive)類型

涉及面試題:原始類型有哪幾種?null 是對象嘛?

在 JS 中,存在着 6 種原始值,分別是:前端

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

首先原始類型存儲的都是值,是沒有函數能夠調用的,好比 undefined.toString()git

此時你確定會有疑問,這不對呀,明明 '1'.toString() 是可使用的。其實在這種狀況下,'1' 已經不是原始類型了,而是被強制轉換成了 String 類型也就是對象類型,因此能夠調用 toString 函數。github

除了會在必要的狀況下強轉類型之外,原始類型還有一些坑。面試

其中 JS 的 number 類型是浮點類型的,在使用中會遇到某些 Bug,好比 0.1 + 0.2 !== 0.3,可是這一塊的內容會在進階部分講到。string 類型是不可變的,不管你在 string 類型上調用何種方法,都不會對值有改變。ajax

另外對於 null 來講,不少人會認爲他是個對象類型,其實這是錯誤的。雖然 typeof null 會輸出 object,可是這只是 JS 存在的一個悠久 Bug。在 JS 的最第一版本中使用的是 32 位系統,爲了性能考慮使用低位存儲變量的類型信息,000 開頭表明是對象,然而 null 表示爲全零,因此將它錯誤的判斷爲 object 。雖然如今的內部類型判斷代碼已經改變了,可是對於這個 Bug 倒是一直流傳下來。算法

對象(Object)類型

涉及面試題:對象類型和原始類型的不一樣之處?函數參數是對象會發生什麼問題?

在 JS 中,除了原始類型那麼其餘的都是對象類型了。對象類型和原始類型不一樣的是,原始類型存儲的是值,對象類型存儲的是地址(指針)。當你建立了一個對象類型的時候,計算機會在內存中幫咱們開闢一個空間來存放值,可是咱們須要找到這個空間,這個空間會擁有一個地址(指針)。編程

const a = []複製代碼

對於常量 a 來講,假設內存地址(指針)爲 #001,那麼在地址 #001 的位置存放了值 [],常量 a存放了地址(指針) #001,再看如下代碼數組

const a = []
const b = a
b.push(1)複製代碼

當咱們將變量賦值給另一個變量時,複製的是本來變量的地址(指針),也就是說當前變量 b 存放的地址(指針)也是 #001,當咱們進行數據修改的時候,就會修改存放在地址(指針) #001 上的值,也就致使了兩個變量的值都發生了改變。promise

接下來咱們來看函數參數是對象的狀況

function test(person) { 
    person.age = 26 person = {
        name: 'yyy',
        age: 30
    }
    return person}const p1 = {
        name: 'yck',
        age: 25
    }
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?複製代碼

對於以上代碼,你是否能正確的寫出結果呢?接下來讓我爲你解析一番:

  • 首先,函數傳參是傳遞對象指針的副本
  • 到函數內部修改參數的屬性這步,我相信你們都知道,當前 p1 的值也被修改了
  • 可是當咱們從新爲 person 分配了一個對象時就出現了分歧

因此最後 person 擁有了一個新的地址(指針),也就和 p1 沒有任何關係了,致使了最終兩個變量的值是不相同的。

typeof vs instanceof

涉及面試題:typeof 是否能正確判斷類型?instanceof 能正確判斷對象的原理是什麼?

typeof 對於原始類型來講,除了 null 均可以顯示正確的類型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof 對於對象來講,除了函數都會顯示 object,因此說 typeof 並不能準確判斷變量究竟是什麼類型
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'複製代碼

若是咱們想判斷一個對象的正確類型,這時候能夠考慮使用 instanceof,由於內部機制是經過原型鏈來判斷的,在後面的章節中咱們也會本身去實現一個 instanceof。

const Person = function() {}
const p1 = new Person()
p1 instanceof
Person // true
var str = 'hello world'
str instanceof
String // false
var str1 = new String('hello world')
str1 instanceof
String // true複製代碼

對於原始類型來講,你想直接經過 instanceof 來判斷類型是不行的,固然咱們仍是有辦法讓 instanceof 判斷原始類型的

class PrimitiveString {
    static [Symbol.hasInstance](x) {
        return typeof x === 'string'
    }
}
console.log('hello world' instanceof PrimitiveString) // true複製代碼

你可能不知道 Symbol.hasInstance 是什麼東西,其實就是一個能讓咱們自定義 instanceof 行爲的東西,以上代碼等同於 typeof 'hello world' === 'string',因此結果天然是 true 了。這其實也側面反映了一個問題, instanceof 也不是百分之百可信的。

類型轉換

涉及面試題:該知識點常在筆試題中見到,熟悉了轉換規則就不害怕此類題目了。

首先咱們要知道,在 JS 中類型轉換隻有三種狀況,分別是:

  • 轉換爲布爾值
  • 轉換爲數字
  • 轉換爲字符串

咱們先來看一個類型轉換表格,而後再進入正題

Boolean

在條件判斷時,除了 undefined, null, false, NaN, '', 0, -0,其餘全部值都轉爲 true,包括全部對象。

對象轉原始類型

對象在轉換類型的時候,會調用內置的 [[ToPrimitive]] 函數,對於該函數來講,算法邏輯通常來講以下:

· 若是已是原始類型了,那就不須要轉換了

· 調用 x.valueOf(),若是轉換爲基礎類型,就返回轉換的值

· 調用 x.toString(),若是轉換爲基礎類型,就返回轉換的值

· 若是都沒有返回原始類型,就會報錯

固然你也能夠重寫 Symbol.toPrimitive ,該方法在轉原始類型時調用優先級最高。

let a = {
    valueOf() {
        return 0
    },
    toString() {
        return '1'
    },
    [Symbol.toPrimitive]() {
        return 2
    }
}
1 + a // => 3複製代碼

四則運算符

加法運算符不一樣於其餘幾個運算符,它有如下幾個特色:

· 運算中其中一方爲字符串,那麼就會把另外一方也轉換爲字符串

· 若是一方不是字符串或者數字,那麼會將它轉換爲數字或者字符串

1 + '1' // '11'
true + true // 2
4 + [1,2,3] //
"41,2,3"複製代碼

若是你對於答案有疑問的話,請看解析:

  • 對於第一行代碼來講,觸發特色一,因此將數字 1 轉換爲字符串,獲得結果 '11'
  • 對於第二行代碼來講,觸發特色二,因此將 true 轉爲數字 1
  • 對於第三行代碼來講,觸發特色二,因此將數組經過 toString 轉爲字符串 1,2,3,獲得結果 41,2,3

另外對於加法還須要注意這個表達式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"複製代碼

由於 + 'b' 等於 NaN,因此結果爲 "aNaN",你可能也會在一些代碼中看到過 + '1' 的形式來快速獲取 number 類型。

那麼對於除了加法的運算符來講,只要其中一方是數字,那麼另外一方就會被轉爲數字

4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN複製代碼

比較運算符

1. 若是是對象,就經過 toPrimitive 轉換對象

2. 若是是字符串,就經過 unicode 字符索引來比較

let a = {
    valueOf() {
        return 0
    },
    toString() {
        return '1'
    }
}
a > -1 // true
在以上代碼中,由於 a 是對象,因此會經過 valueOf 轉換爲原始類型再比較值。複製代碼

this

涉及面試題:如何正確判斷 this?箭頭函數的 this 是什麼?

this 是不少人會混淆的概念,可是其實它一點都不難,只是網上不少文章把簡單的東西說複雜了。在這一小節中,你必定會完全明白 this 這個概念的。

咱們先來看幾個函數調用的場景

function foo() {
    console.log(this.a)
}
var a = 1
foo()
const obj = {
    a: 2,
    foo: foo
}
obj.foo()
const c = new foo()複製代碼

接下來咱們一個個分析上面幾個場景

  • 對於直接調用 foo 來講,無論 foo 函數被放在了什麼地方,this 必定是 window
  • 對於 obj.foo() 來講,咱們只須要記住,誰調用了函數,誰就是 this,因此在這個場景下 foo函數中的 this 就是 obj 對象
  • 對於 new 的方式來講,this 被永遠綁定在了 c 上面,不會被任何方式改變 this

說完了以上幾種狀況,其實不少代碼中的 this 應該就沒什麼問題了,下面讓咱們看看箭頭函數中的 this

function a() {
    return () => {
        return () => {
            console.log(this)
        }
    }
}
console.log(a()()())複製代碼

首先箭頭函數實際上是沒有 this 的,箭頭函數中的 this 只取決包裹箭頭函數的第一個普通函數的 this。在這個例子中,由於包裹箭頭函數的第一個普通函數是 a,因此此時的 this 是 window。另外對箭頭函數使用 bind 這類函數是無效的。

最後種狀況也就是 bind 這些改變上下文的 API 了,對於這些函數來講,this 取決於第一個參數,若是第一個參數爲空,那麼就是 window。

那麼說到 bind,不知道你們是否考慮過,若是對一個函數進行屢次 bind,那麼上下文會是什麼呢?

let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)()
// => ?複製代碼

若是你認爲輸出結果是 a,那麼你就錯了,其實咱們能夠把上述代碼轉換成另外一種形式

//fn.bind().bind(a) 等於
let fn2 = function fn1() {
    return function() {
        return fn.apply()
    }.apply(a)
}
fn2()複製代碼

能夠從上述代碼中發現,無論咱們給函數 bind 幾回,fn 中的 this 永遠由第一次 bind 決定,因此結果永遠是 window。

let a = { 
    name: 'yck' 
}
function foo() {
    console.log(this.name)
}
foo.bind(a)() // => 'yck'複製代碼

以上就是 this 的規則了,可是可能會發生多個規則同時出現的狀況,這時候不一樣的規則之間會根據優先級最高的來決定 this 最終指向哪裏。

首先,new 的方式優先級最高,接下來是 bind 這些函數,而後是 obj.foo() 這種調用方式,最後是 foo 這種調用方式,同時,箭頭函數的 this 一旦被綁定,就不會再被任何方式所改變。

小結

以上就是咱們 JS 基礎知識點的第一部份內容了。這一小節中涉及到的知識點在咱們平常的開發中常常能夠看到,而且不少容易出現的坑 也出自於這些知識點,相信認真讀完的你必定會在往後的開發中少踩不少坑。若是你們對於這個章節的內容存在疑問,歡迎在評論區與我互動。


JS 基礎知識點及常考面試題(二)

在這一章節中咱們繼續來了解 JS 的一些常考和容易混亂的基礎知識點。

== vs ===

涉及面試題:== 和 === 有什麼區別?

對於 == 來講,若是對比雙方的類型不同的話,就會進行類型轉換,這也就用到了咱們上一章節講的內容。

假如咱們須要對比 x 和 y 是否相同,就會進行以下判斷流程:

1. 首先會判斷二者類型是否相同。相同的話就是比大小了

2. 類型不相同的話,那麼就會進行類型轉換

3. 會先判斷是否在對比 null 和 undefined,是的話就會返回 true

4. 判斷二者類型是否爲 string 和 number,是的話就會將字符串轉換爲 number

5. 1 == '1'

6. ↓

7. 1 == 1

8. 判斷其中一方是否爲 boolean,是的話就會把 boolean 轉爲 number 再進行判斷

9. '1' == true

10. ↓

11. '1' == 1

12. ↓

13. 1 == 1

14. 判斷其中一方是否爲 object 且另外一方爲 string、number 或者 symbol,是的話就會把 object 轉爲原始類型再進行判斷

15. '1' == { name: 'yck' }

16. ↓

17. '1' == '[object Object]'

思考題:看完了上面的步驟,對於 [] == ![] 你是否能正確寫出答案呢?

固然了,這個流程圖並無將全部的狀況都列舉出來,我這裏只將經常使用到的狀況列舉了,若是你想了解更多的內容能夠參考 標準文檔

對於 === 來講就簡單多了,就是判斷二者類型和值是否相同。

閉包

涉及面試題:什麼是閉包?

閉包的定義其實很簡單:函數 A 內部有一個函數 B,函數 B 能夠訪問到函數 A 中的變量,那麼函數 B 就是閉包。

function A() {
    let a = 1
    window.B = function() {
        console.log(a)
    }
}
A()
B() // 1複製代碼

不少人對於閉包的解釋多是函數嵌套了函數,而後返回一個函數。其實這個解釋是不完整的,就好比我上面這個例子就能夠反駁這個觀點。

在 JS 中,閉包存在的意義就是讓咱們能夠間接訪問函數內部的變量。

經典面試題,循環中使用閉包解決 `var` 定義函數的問題

for (var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i)
    }, i * 1000)
}複製代碼

首先由於 setTimeout 是個異步函數,因此會先把循環所有執行完畢,這時候 i 就是 6 了,因此會輸出一堆 6。

解決辦法有三種,第一種是使用閉包的方式

for (var i = 1; i <= 5; i++) {
    (function(j) {
        setTimeout(function timer() {
            console.log(j)
        }, j * 1000)
    })(i)
}複製代碼

在上述代碼中,咱們首先使用了當即執行函數將 i 傳入函數內部,這個時候值就被固定在了參數 j上面不會改變,當下次執行 timer 這個閉包的時候,就可使用外部函數的變量 j,從而達到目的。

第二種就是使用 setTimeout 的第三個參數,這個參數會被當成 timer 函數的參數傳入。

for (var i = 1; i <= 5; i++) {
    setTimeout(
        function timer(j) {
            console.log(j)
    },i * 1000,i)
}複製代碼

第三種就是使用 let 定義 i 了來解決問題了,這個也是最爲推薦的方式

for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i)
    }, i * 1000)
}複製代碼

深淺拷貝

涉及面試題:什麼是淺拷貝?如何實現淺拷貝?什麼是深拷貝?如何實現深拷貝?

在上一章節中,咱們瞭解了對象類型在賦值的過程當中實際上是複製了地址,從而會致使改變了一方其餘也都被改變的狀況。一般在開發中咱們不但願出現這樣的問題,咱們可使用淺拷貝來解決這個狀況。

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2複製代碼

淺拷貝

首先能夠經過 Object.assign 來解決這個問題,不少人認爲這個函數是用來深拷貝的。其實並非,Object.assign 只會拷貝全部的屬性值到新的對象中,若是屬性值是對象的話,拷貝的是地址,因此並非深拷貝。

let a = {

  age: 1

}

let b = Object.assign({}, a)

a.age = 2

console.log(b.age) // 1複製代碼

另外咱們還能夠經過展開運算符 ... 來實現淺拷貝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1複製代碼

一般淺拷貝就能解決大部分問題了,可是當咱們遇到以下狀況就可能須要使用到深拷貝了

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native複製代碼

淺拷貝只解決了第一層的問題,若是接下去的值中還有對象的話,那麼就又回到最開始的話題了,二者享有相同的地址。要解決這個問題,咱們就得使用深拷貝了。

深拷貝

這個問題一般能夠經過 JSON.parse(JSON.stringify(object)) 來解決。

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE複製代碼

可是該方法也是有侷限性的:

  • 會忽略 undefined
  • 會忽略 symbol
  • 不能序列化函數
  • 不能解決循環引用的對象

let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3,
    },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)複製代碼

若是你有這麼一個循環引用對象,你會發現並不能經過該方法實現深拷貝

在遇到函數、 undefined 或者 symbol 的時候,該對象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}複製代碼

你會發如今上述狀況中,該方法會忽略掉函數和 undefined 。

可是在一般狀況下,複雜數據都是能夠序列化的,因此這個函數能夠解決大部分問題。

若是你所需拷貝的對象含有內置類型而且不包含函數,可使用 MessageChannel

function structuralClone(obj) {
    return new Promise(resolve => {
        const { port1, port2 } = new
        MessageChannel()
        port2.onmessage = ev => resolve(ev.data)
        port1.postMessage(obj)
    })
}
var obj = {
    a: 1,
    b: {
        c: 2
    }
}
obj.b.d = obj.b
// 注意該方法是異步的
// 能夠處理 undefined 和循環引用對象
const test = async () => {
    const clone = await
    structuralClone(obj)
    console.log(clone)
}
test()複製代碼

固然你可能想本身來實現一個深拷貝,可是其實實現一個深拷貝是很困難的,須要咱們考慮好多種邊界狀況,好比原型鏈如何處理、DOM 如何處理等等,因此這裏咱們實現的深拷貝只是簡易版,而且我其實更推薦使用 lodash 的深拷貝函數

function deepClone(obj) {
    function isObject(o) {
        return (typeof o === 'object' || typeof o === 'function') && o !== null
    }
    if (!isObject(obj)) {
        throw new Error('非對象')
    }
    let isArray = Array.isArray(obj)
    let newObj = isArray ? [...obj] : {...obj }
    Reflect.ownKeys(newObj).forEach(key=> {
        newObj[key] = isObject(obj[key]) ?deepClone(obj[key]) : obj[key]
    })
    return newObj
}
let obj = {
    a: [1, 2, 3],
    b: {
        c: 2,
        d: 3
    }
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2複製代碼

原型

涉及面試題:如何理解原型?如何理解原型鏈?

當咱們建立一個對象時 let obj = { age: 25 },咱們能夠發現能使用不少種函數,可是咱們明明沒有定義過它們,對於這種狀況你是否有過疑惑?

當咱們在瀏覽器中打印 obj 時你會發現,在 obj 上竟然還有一個 __proto__ 屬性,那麼看來以前的疑問就和這個屬性有關係了。

其實每一個 JS 對象都有 __proto__ 屬性,這個屬性指向了原型。這個屬性在如今來講已經不推薦直接去使用它了,這只是瀏覽器在早期爲了讓咱們訪問到內部屬性 [[prototype]] 來實現的一個東西。

講到這裏好像仍是沒有弄明白什麼是原型,接下來讓咱們再看看 __proto__ 裏面有什麼吧。

看到這裏你應該明白了,原型也是一個對象,而且這個對象中包含了不少函數,因此咱們能夠得出一個結論:對於 obj 來講,能夠經過 __proto__ 找到一個原型對象,在該對象中定義了不少函數讓咱們來使用。

在上面的圖中咱們還能夠發現一個 constructor 屬性,也就是構造函數

打開 constructor 屬性咱們又能夠發現其中還有一個 prototype 屬性,而且這個屬性對應的值和先前咱們在 __proto__ 中看到的如出一轍。因此咱們又能夠得出一個結論:原型的 constructor 屬性指向構造函數,構造函數又經過 prototype 屬性指回原型,可是並非全部函數都具備這個屬性,Function.prototype.bind() 就沒有這個屬性。

其實原型就是那麼簡單,接下來咱們再來看一張圖,相信這張圖能讓你完全明白原型和原型鏈

看完這張圖,我再來解釋下什麼是原型鏈吧。其實原型鏈就是多個對象經過 __proto__ 的方式鏈接了起來。爲何 obj 能夠訪問到 valueOf 函數,就是由於 obj 經過原型鏈找到了 valueOf 函數。

對於這一小節的知識點,總結起來就是如下幾點:

  • Object 是全部對象的爸爸,全部對象均可以經過 __proto__ 找到它
  • Function 是全部函數的爸爸,全部函數均可以經過 __proto__ 找到它
  • 函數的 prototype 是一個對象
  • 對象的 __proto__ 屬性指向原型, __proto__ 將對象和原型鏈接起來組成了原型鏈

若是你還想深刻學習原型這部分的內容,能夠閱讀我以前寫的文章

小結

以上就是所有的常考和容易混亂的基礎知識點了,下一章節咱們將會學習 ES6 部分的知識。若是你們對於這個章節的內容存在疑問,歡迎在評論區與我互動。

ES6 知識點及常考面試題

本章節咱們未來學習 ES6 部分的內容。

var、let 及 const 區別

涉及面試題:什麼是提高?什麼是暫時性死區?var、let 及 const 區別?

對於這個問題,咱們應該先來了解提高(hoisting)這個概念。

console.log(a) // undefined
var a = 1複製代碼

從上述代碼中咱們能夠發現,雖然變量尚未被聲明,可是咱們卻可使用這個未被聲明的變量,這種狀況就叫作提高,而且提高的是聲明。

對於這種狀況,咱們能夠把代碼這樣來看

var a
console.log(a) // undefined
a = 1複製代碼

接下來咱們再來看一個例子

var a = 10
var a
console.log(a)複製代碼

對於這個例子,若是你認爲打印的值爲 undefined 那麼就錯了,答案應該是 10,對於這種狀況,咱們這樣來看代碼

var a
var a
a = 10
console.log(a)複製代碼

到這裏爲止,咱們已經瞭解了 var 聲明的變量會發生提高的狀況,其實不只變量會提高函數也會被提高。

console.log(a) // ƒ a() {}
function a() {}
var a = 1複製代碼

對於上述代碼,打印結果會是 ƒ a() {},即便變量聲明在函數以後,這也說明了函數會被提高,而且優先於變量提高。

說完了這些,想必你們也知道 var 存在的問題了,使用 var 聲明的變量會被提高到做用域的頂部,接下來咱們再來看 let 和 const 。

咱們先來看一個例子:

var a = 1
let b = 1
const c = 1
console.log(window.b) // undefined
console.log(window. c) // undefined
function test(){
    console.log(a)
    let a
}
test()複製代碼

首先在全局做用域下使用 let 和 const 聲明變量,變量並不會被掛載到 window 上,這一點就和 var 聲明有了區別。

再者當咱們在聲明 a 以前若是使用了 a,就會出現報錯的狀況

你可能會認爲這裏也出現了提高的狀況,可是由於某些緣由致使不能訪問。

首先報錯的緣由是由於存在暫時性死區,咱們不能在聲明前就使用變量,這也是 let 和 const 優於 var 的一點。而後這裏你認爲的提高和 var 的提高是有區別的,雖然變量在編譯的環節中被告知在這塊做用域中能夠訪問,可是訪問是受限制的。

那麼到這裏,想必你們也都明白 var、let 及 const 區別了,不知道你是否會有這麼一個疑問,爲何要存在提高這個事情呢,其實提高存在的根本緣由就是爲了解決函數間互相調用的狀況

function test1() {
    test2()
}
function test2() {
    test1()
}
test1()複製代碼

假如不存在提高這個狀況,那麼就實現不了上述的代碼,由於不可能存在 test1 在 test2 前面而後 test2 又在 test1 前面。

那麼最後咱們總結下這小節的內容:

· 函數提高優先於變量提高,函數提高會把整個函數挪到做用域頂部,變量提高只會把聲明挪到做用域頂部

  • var 存在提高,咱們能在聲明以前使用。let、const 由於暫時性死區的緣由,不能在聲明前使用
  • var 在全局做用域下聲明變量會致使變量掛載在 window 上,其餘二者不會
  • let 和 const 做用基本一致,可是後者聲明的變量不能再次賦值

原型繼承和 Class 繼承

涉及面試題:原型如何實現繼承?Class 如何實現繼承?Class 本質是什麼?

首先先來說下 class,其實在 JS 中並不存在類,class 只是語法糖,本質仍是函數。

class Person {}
Person instanceof
Function // true複製代碼

在上一章節中咱們講解了原型的知識點,在這一小節中咱們將會分別使用原型和 class 的方式來實現繼承。

組合繼承

組合繼承是最經常使用的繼承方式,

function Parent(value) {
    this.val = value
}
Parent.prototype.getValue= function() {
    console.log(this.val)
}
function Child(value) {
    Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof
Parent // true複製代碼

以上繼承的方式核心是在子類的構造函數中經過 Parent.call(this) 繼承父類的屬性,而後改變子類的原型爲 new Parent() 來繼承父類的函數。

這種繼承方式優勢在於構造函數能夠傳參,不會與父類引用屬性共享,能夠複用父類的函數,可是也存在一個缺點就是在繼承父類函數的時候調用了父類構造函數,致使子類的原型上多了不須要的父類屬性,存在內存上的浪費。

寄生組合繼承

這種繼承方式對組合繼承進行了優化,組合繼承缺點在於繼承父類函數時調用了構造函數,咱們只須要優化掉這點就好了。

function Parent(value) {
    this.val = value
}
Parent.prototype.getValue= function() {
    console.log(this.val)
}
function Child(value) {
    Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
    constructor: {
        value: Child,
        enumerable: false,
        writable: true,
        configurable: true
    }
})
const child = new Child(1)
child.getValue() // 1
child instanceof
Parent // true複製代碼

以上繼承實現的核心就是將父類的原型賦值給了子類,而且將構造函數設置爲子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的構造函數。

Class 繼承

以上兩種繼承方式都是經過原型去解決的,在 ES6 中,咱們可使用 class 去實現繼承,而且實現起來很簡單

class Parent {
    constructor(value) {
        this.val = value
    }
    getValue() {
        console.log(this.val)
    }
}
class Child extends Parent {
    constructor(value) {
        super(value)
        this.val = value
    }
}
let child = new Child(1)
child.getValue() // 1
child instanceof
Parent // true複製代碼

class 實現繼承的核心在於使用 extends 代表繼承自哪一個父類,而且在子類構造函數中必須調用 super,由於這段代碼能夠當作 Parent.call(this, value)。

固然了,以前也說了在 JS 中並不存在類,class 的本質就是函數。

模塊化

涉及面試題:爲何要使用模塊化?都有哪幾種方式能夠實現模塊化,各有什麼特色?

使用一個技術確定是有緣由的,那麼使用模塊化能夠給咱們帶來如下好處

  • 解決命名衝突
  • 提供複用性
  • 提升代碼可維護性

當即執行函數

在早期,使用當即執行函數實現模塊化是常見的手段,經過函數做用域解決了命名衝突、污染全局做用域的問題

(function(globalVariable){
    globalVariable.test = function() {}
    // ... 聲明各類變量、函數都不會污染全局做用域
})(globalVariable)複製代碼

AMD CMD

鑑於目前這兩種實現方式已經不多見到,因此再也不對具體特性細聊,只須要了解這二者是如何使用的。

// AMD
define(['./a', './b'], function(a,b) {
    // 加載模塊完畢可使用
    a.do()
    b.do()
})
// CMD
define(function(require,exports, module) {
    // 加載模塊
    // 能夠把 require 寫在函數體的任意地方實現延遲加載
    var a = require('./a')
    a.doSomething()
})複製代碼

CommonJS

CommonJS 最先是 Node 在使用,目前也仍然普遍使用,好比在 Webpack 中你就能見到它,固然目前在 Node 中的模塊管理已經和 CommonJS 有一些區別了。

// a.js
module.exports = {
    a: 1
}
// or 
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
由於 CommonJS 仍是會使用到的,因此這裏會對一些疑難點進行解析
先說 require 吧
var module = require('./a.js')
module.a 
// 這裏其實就是包裝了一層當即執行函數,這樣就不會污染全局變量了,
// 重要的是 module 這裏,module 是 Node 獨有的一個變量
module.exports = {
    a: 1
}
// module 基本實現
var module = {
    id: 'xxxx', // 我總得知道怎麼去找到他吧
    exports: {} // exports 就是個空對象
}
// 這個是爲何 exports 和 module.exports 用法類似的緣由
var exports = module.exports 
var load = function (module) {
    // 導出的東西
    var a = 1
    module.exports = a
    return module.exports
};
// 而後當我 require 的時候去找到獨特的
// id,而後將要使用的東西用當即執行函數包裝下,over複製代碼

另外雖然 exports 和 module.exports 用法類似,可是不能對 exports 直接賦值。由於 var exports = module.exports 這句代碼代表了 exports 和 module.exports 享有相同地址,經過改變對象的屬性值會對二者都起效,可是若是直接對 exports 賦值就會致使二者再也不指向同一個內存地址,修改並不會對 module.exports 起效。

ES Module

ES Module 是原生實現的模塊化方案,與 CommonJS 有如下幾個區別

  • CommonJS 支持動態導入,也就是 require(${path}/xx.js),後者目前不支持,可是已有提案
  • CommonJS 是同步導入,由於用於服務端,文件都在本地,同步導入即便卡住主線程影響也不大。然後者是異步導入,由於用於瀏覽器,須要下載文件,若是也採用同步導入會對渲染有很大影響
  • CommonJS 在導出時都是值拷貝,就算導出的值變了,導入的值也不會改變,因此若是想更新值,必須從新導入一次。可是 ES Module 採用實時綁定的方式,導入導出的值都指向同一個內存地址,因此導入值會跟隨導出值變化
  • ES Module 會編譯成 require/exports 來執行的

// 引入模塊 API
import XXX from './a.js'
import { XXX } from './a.js'
// 導出模塊 API
export function a() {}
export default function() {}複製代碼

Proxy

涉及面試題:Proxy 能夠實現什麼功能?

若是你平時有關注 Vue 的進展的話,可能已經知道了在 Vue3.0 中將會經過 Proxy 來替換本來的 Object.defineProperty 來實現數據響應式。 Proxy 是 ES6 中新增的功能,它能夠用來自定義對象中的操做。

let p = new Proxy(target, handler)複製代碼

target 表明須要添加代理的對象,handler 用來自定義對象中的操做,好比能夠用來自定義 set 或者 get 函數。

接下來咱們經過 Proxy 來實現一個數據響應式

let onWatch = (obj, setBind, getLogger)=> {
    let handler = {
        get(target, property, receiver) {
           getLogger(target, property)
            return Reflect.get(target, property, receiver)
        },
        set(target, property, value, receiver) {
            setBind(value, property)
            return Reflect.set(target, property, value)
        }
    }
    return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(obj,(v, property) => {
    console.log(`監聽到屬性${property}改變爲${v}`)
},(target, property) => {
        console.log(`'${property}' = ${target[property]}`)
})
p.a = 2 // 監聽到屬性a改變
p.a // 'a' = 2複製代碼

在上述代碼中,咱們經過自定義 set 和 get 函數的方式,在本來的邏輯中插入了咱們的函數邏輯,實現了在對對象任何屬性進行讀寫時發出通知。

固然這是簡單版的響應式實現,若是須要實現一個 Vue 中的響應式,須要咱們在 get 中收集依賴,在 set 派發更新,之因此 Vue3.0 要使用 Proxy 替換本來的 API 緣由在於 Proxy 無需一層層遞歸爲每一個屬性添加代理,一次便可完成以上操做,性能上更好,而且本來的實現有一些數據更新不能監聽到,可是 Proxy 能夠完美監聽到任何方式的數據改變,惟一缺陷可能就是瀏覽器的兼容性很差了。

map, filter, reduce

涉及面試題:map, filter, reduce 各自有什麼做用?

map 做用是生成一個新數組,遍歷原數組,將每一個元素拿出來作一些變換而後放入到新的數組中。

[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
另外 map 的回調函數接受三個參數,分別是當前索引元素,索引,原數組
['1','2','3'].map(parseInt)
第一輪遍歷 parseInt('1', 0) -> 1
第二輪遍歷 parseInt('2', 1) -> NaN
第三輪遍歷 parseInt('3', 2) -> NaN複製代碼

filter 的做用也是生成一個新數組,在遍歷數組的時候將返回值爲 true 的元素放入新數組,咱們能夠利用這個函數刪除一些不須要的元素

let array = [1, 2, 4, 6]
let newArray = array.filter(item => item
!== 6)
console.log(newArray) // [1, 2, 4]複製代碼

和 map 同樣,filter 的回調函數也接受三個參數,用處也相同。

最後咱們來說解 reduce 這塊的內容,同時也是最難理解的一塊內容。reduce 能夠將數組中的元素經過回調函數最終轉換爲一個值。

若是咱們想實現一個功能將函數裏的元素所有相加獲得一個值,可能會這樣寫代碼

const arr = [1, 2, 3]
let total = 0
for (let i = 0; i < arr.length; i++) {
    total += arr[i]
}
console.log(total) //6複製代碼

可是若是咱們使用 reduce 的話就能夠將遍歷部分的代碼優化爲一行代碼

const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) =>
acc + current, 0)
console.log(sum)複製代碼

對於 reduce 來講,它接受兩個參數,分別是回調函數和初始值,接下來咱們來分解上述代碼中 reduce 的過程

  • 首先初始值爲 0,該值會在執行第一次回調函數時做爲第一個參數傳入
  • 回調函數接受四個參數,分別爲累計值、當前元素、當前索引、原數組,後三者想必你們均可以明白做用,這裏着重分析第一個參數
  • 在一次執行回調函數時,當前值和初始值相加得出結果 1,該結果會在第二次執行回調函數時當作第一個參數傳入
  • 因此在第二次執行回調函數時,相加的值就分別是 1 和 2,以此類推,循環結束後獲得結果 6

想必經過以上的解析你們應該明白 reduce 是如何經過回調函數將全部元素最終轉換爲一個值的,固然 reduce 還能夠實現不少功能,接下來咱們就經過 reduce 來實現 map 函數

const arr = [1, 2, 3]
const mapArray = arr.map(value => value * 2)
const reduceArray = arr.reduce((acc, current)=> {
    acc.push(current * 2)
    return acc
}, [])
console.log(mapArray, reduceArray) // [2, 4, 6]複製代碼

若是你對這個實現還有困惑的話,能夠根據上一步的解析步驟來分析過程。

小結

這一章節咱們瞭解了部分 ES6 常考的知識點,其餘的一些異步內容咱們會放在下一章節去講。若是你們對於這個章節的內容存在疑問,歡迎在評論區與我互動。

JS 異步編程及常考面試題

在上一章節中咱們瞭解了常見 ES6 語法的一些知識點。這一章節咱們將會學習異步編程這一塊的內容,鑑於異步編程是 JS 中相當重要的內容,因此咱們將會用三個章節來學習異步編程涉及到的重點和難點,同時這一塊內容也是面試常考範圍,但願你們認真學習。

併發(concurrency)和並行(parallelism)區別

涉及面試題:併發與並行的區別?

異步和這小節的知識點其實並非一個概念,可是這兩個名詞確實是不少人都常會混淆的知識點。其實混淆的緣由可能只是兩個名詞在中文上的類似,在英文上來講徹底是不一樣的單詞。

併發是宏觀概念,我分別有任務 A 和任務 B,在一段時間內經過任務間的切換完成了這兩個任務,這種狀況就能夠稱之爲併發。

並行是微觀概念,假設 CPU 中存在兩個核心,那麼我就能夠同時完成任務 A、B。同時完成多個任務的狀況就能夠稱之爲並行。

回調函數(Callback)

涉及面試題:什麼是回調函數?回調函數有什麼缺點?如何解決回調地獄問題?

回調函數應該是你們常用到的,如下代碼就是一個回調函數的例子:

ajax(url, () => {
    // 處理邏輯
})複製代碼

可是回調函數有一個致命的弱點,就是容易寫出回調地獄(Callback hell)。假設多個請求存在依賴性,你可能就會寫出以下代碼:

ajax(url, () => {
    // 處理邏輯
    ajax(url1, () => {
        // 處理邏輯
        ajax(url2,() => {
            // 處理邏輯
        })
    })
})複製代碼

以上代碼看起來不利於閱讀和維護,固然,你可能會想說解決這個問題還不簡單,把函數分開來寫不就得了

function firstAjax() {
    ajax(url1,() => {
        // 處理邏輯
        secondAjax()
    })
}
function secondAjax() {
    ajax(url2,() => {
        // 處理邏輯
    })
}
ajax(url, () => {
    // 處理邏輯
    firstAjax()
})複製代碼

以上的代碼雖然看上去利於閱讀了,可是仍是沒有解決根本問題。

回調地獄的根本問題就是:

1. 嵌套函數存在耦合性,一旦有所改動,就會牽一髮而動全身

2. 嵌套函數一多,就很難處理錯誤

固然,回調函數還存在着別的幾個缺點,好比不能使用 try catch 捕獲錯誤,不能直接 return。在接下來的幾小節中,咱們未來學習經過別的技術解決這些問題。

Generator

涉及面試題:你理解的 Generator 是什麼?

Generator 算是 ES6 中難理解的概念之一了,Generator 最大的特色就是能夠控制函數的執行。在這一小節中咱們不會去講什麼是 Generator,而是把重點放在 Generator 的一些容易困惑的地方。

function *foo(x) {
    let y = 2 * (yield (x + 1))
    let z = yield (y / 3)
    return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8,
done: false}
console.log(it.next(13)) // => {value:
42, done: true}複製代碼

你也許會疑惑爲何會產生與你預想不一樣的值,接下來就讓我爲你逐行代碼分析緣由

  • 首先 Generator 函數調用和普通函數不一樣,它會返回一個迭代器
  • 當執行第一次 next 時,傳參會被忽略,而且函數暫停在 yield (x + 1) 處,因此返回 5 + 1 = 6
  • 當執行第二次 next 時,傳入的參數等於上一個 yield 的返回值,若是你不傳參,yield 永遠返回 undefined。此時 let y = 2 * 12,因此第二個 yield 等於 2 * 12 / 3 = 8
  • 當執行第三次 next 時,傳入的參數會傳遞給 z,因此 z = 13, x = 5, y = 24,相加等於 42

Generator 函數通常見到的很少,其實也於他有點繞有關係,而且通常會配合 co 庫去使用。固然,咱們能夠經過 Generator 函數解決回調地獄的問題,能夠把以前的回調地獄例子改寫爲以下代碼:

function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()複製代碼

Promise

涉及面試題:Promise 的特色是什麼,分別有什麼優缺點?什麼是 Promise 鏈?Promise 構造函數執行和 then 函數執行有什麼區別?

Promise 翻譯過來就是承諾的意思,這個承諾會在將來有一個確切的答覆,而且該承諾有三種狀態,分別是:

1. 等待中(pending)

2. 完成了 (resolved)

3. 拒絕了(rejected)

這個承諾一旦從等待狀態變成爲其餘狀態就永遠不能更改狀態了,也就是說一旦狀態變爲 resolved 後,就不能再次改變

new Promise((resolve,reject) => {
    resolve('success')
    // 無效
    reject('reject')
})複製代碼

當咱們在構造 Promise 的時候,構造函數內部的代碼是當即執行的

new Promise((resolve, reject) => {
    console.log('new Promise')
    resolve('success')
})
console.log('finifsh')
// new Promise
-> finifsh複製代碼

Promise 實現了鏈式調用,也就是說每次調用 then 以後返回的都是一個 Promise,而且是一個全新的 Promise,緣由也是由於狀態不可變。若是你在 then 中 使用了 return,那麼 return 的值會被 Promise.resolve() 包裝

Promise.resolve(1)
.then(res => {
    console.log(res) // => 1
    return 2 // 包裝成 Promise.resolve(2)
})
.then(res => {
    console.log(res) // => 2
})複製代碼

固然了,Promise 也很好地解決了回調地獄的問題,能夠把以前的回調地獄例子改寫爲以下代碼:

ajax(url)
.then(res => {
    console.log(res)
    return ajax(url1)
}).then(res => {
    console.log(res)
    return ajax(url2)
}).then(res => console.log(res))複製代碼

前面都是在講述 Promise 的一些優勢和特色,其實它也是存在一些缺點的,好比沒法取消 Promise,錯誤須要經過回調函數捕獲。

async 及 await

涉及面試題:async 及 await 的特色,它們的優勢和缺點分別是什麼?await 原理是什麼?

一個函數若是加上 async ,那麼該函數就會返回一個 Promise

async function test() {
    return "1"
}
console.log(test()) // -> Promise {<resolved>:"1"}複製代碼

async 就是將函數返回值使用 Promise.resolve() 包裹了下,和 then 中處理返回值同樣,而且 await 只能配套 async 使用

async function test() {
    let value = await sleep()
}複製代碼

async 和 await 能夠說是異步終極解決方案了,相比直接使用 Promise 來講,優點在於處理 then的調用鏈,可以更清晰準確的寫出代碼,畢竟寫一大堆 then 也很噁心,而且也能優雅地解決回調地獄問題。固然也存在一些缺點,由於 await 將異步代碼改形成了同步代碼,若是多個異步代碼沒有依賴性卻使用了 await 會致使性能上的下降。

async function test() {
    // 如下代碼沒有依賴性的話,徹底可使用 Promise.all 的方式
    // 若是有依賴性的話,其實就是解決回調地獄的例子了
    await fetch(url)
    await fetch(url1)
    await fetch(url2)
}複製代碼

下面來看一個使用 await 的例子:

let a = 0
let b = async () => {
    a = a + await 10
    console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1複製代碼

對於以上代碼你可能會有疑惑,讓我來解釋下緣由

  • 首先函數 b 先執行,在執行到 await 10 以前變量 a 仍是 0,由於 await 內部實現了 generator ,generator 會保留堆棧中東西,因此這時候 a = 0 被保存了下來
  • 由於 await 是異步操做,後來的表達式不返回 Promise 的話,就會包裝成 Promise.reslove(返回值),而後會去執行函數外的同步代碼
  • 同步代碼執行完畢後開始執行異步代碼,將保存下來的值拿出來使用,這時候 a = 0 + 10

上述解釋中提到了 await 內部實現了 generator,其實 await 就是 generator 加上 Promise 的語法糖,且內部實現了自動執行 generator。若是你熟悉 co 的話,其實本身就能夠實現這樣的語法糖。

經常使用定時器函數

涉及面試題:setTimeout、setInterval、requestAnimationFrame 各有什麼特色?

異步編程固然少不了定時器了,常見的定時器函數有 setTimeout、setInterval、requestAnimationFrame。咱們先來說講最經常使用的setTimeout,不少人認爲 setTimeout 是延時多久,那就應該是多久後執行。

其實這個觀點是錯誤的,由於 JS 是單線程執行的,若是前面的代碼影響了性能,就會致使 setTimeout 不會定期執行。固然了,咱們能夠經過代碼去修正 setTimeout,從而使定時器相對準確

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
    count++
    // 代碼執行所消耗的時間
    let offset = new Date().getTime() - (startTime + count *interval);
    let diff = end - new Date().getTime()
    let h = Math.floor(diff / (60 * 1000 * 60))
    let hdiff = diff % (60 * 1000 * 60)
    let m = Math.floor(hdiff / (60 * 1000))
    let mdiff = hdiff % (60 * 1000)
    let s = mdiff / (1000)
    let sCeil = Math.ceil(s)
    let sFloor = Math.floor(s)
    // 獲得下一次循環所消耗的時間
    currentInterval = interval - offset 
    console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執行時間:'+offset, '下次循環間隔'+currentInterval) 
    setTimeout(loop, currentInterval)
}
setTimeout(loop,currentInterval)複製代碼

接下來咱們來看 setInterval,其實這個函數做用和 setTimeout 基本一致,只是該函數是每隔一段時間執行一次回調函數。

一般來講不建議使用 setInterval。第一,它和 setTimeout 同樣,不能保證在預期的時間執行任務。第二,它存在執行累積的問題,請看如下僞代碼

function demo() {
    setInterval(function(){
        console.log(2)
    },1000)
    sleep(2000)
}
demo()複製代碼

以上代碼在瀏覽器環境中,若是定時器執行過程當中出現了耗時操做,多個回調函數會在耗時操做結束之後同時執行,這樣可能就會帶來性能上的問題。

若是你有循環定時器的需求,其實徹底能夠經過 requestAnimationFrame 來實現

function setInterval(callback, interval) {
    let timer
    const now = Date.now
    let startTime = now()
    let endTime = startTime
    const loop = () => {
        timer = window.requestAnimationFrame(loop)
        endTime = now()
        if (endTime - startTime >=interval) {
            startTime = endTime = now()
            callback(timer)
        }
    }
timer = window.requestAnimationFrame(loop)
    return timer
}
let a = 0
setInterval(timer=> {
    console.log(1)
    a++
    if (a === 3) cancelAnimationFrame(timer)
}, 1000)複製代碼

首先 requestAnimationFrame 自帶函數節流功能,基本能夠保證在 16.6 毫秒內只執行一次(不掉幀的狀況下),而且該函數的延時效果是精確的,沒有其餘定時器時間不許的問題,固然你也能夠經過該函數來實現 setTimeout。

小結

異步編程是 JS 中較難掌握的內容,同時也是很重要的知識點。以上提到的每一個知識點其實均可以做爲一道面試題,但願你們能夠好好掌握以上內容若是你們對於這個章節的內容存在疑問,歡迎在評論區與我互動。

JS 進階知識點及常考面試題

在這一章節中,咱們將會學習到一些原理相關的知識,不會解釋涉及到的知識點的做用及用法,若是你們對於這些內容還不怎麼熟悉,推薦先去學習相關的知識點內容再來學習原理知識。

手寫 call、apply 及 bind 函數

涉及面試題:call、apply 及 bind 函數內部實現是怎麼樣的?

首先從如下幾點來考慮如何實現這幾個函數

  • 不傳入第一個參數,那麼上下文默認爲 window
  • 改變了 this 指向,讓新的對象能夠執行該函數,並能接受參數

那麼咱們先來實現 call

Function.prototype.myCall = function(context){
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    context = context || window
    context.fn = this
    const args = [...arguments].slice(1)
    const result = context.fn(...args)
    delete context.fn
    return result
}複製代碼

如下是對實現的分析:

  • 首先 context 爲可選參數,若是不傳的話默認上下文爲 window
  • 接下來給 context 建立一個 fn 屬性,並將值設置爲須要調用的函數
  • 由於 call 能夠傳入多個參數做爲調用函數的參數,因此須要將參數剝離出來
  • 而後調用函數並將對象上的函數刪除

以上就是實現 call 的思路,apply 的實現也相似,區別在於對參數的處理,因此就不一一分析思路了

Function.prototype.myApply = function(context){
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    context = context || window
    context.fn = this
    let result
    // 處理參數和 call 有區別
    if (arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn
    return
    result
}複製代碼

bind 的實現對比其餘兩個函數略微地複雜了一點,由於 bind 須要返回一個函數,須要判斷一些邊界問題,如下是 bind 的實現

Function.prototype.myBind = function(context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    const _this = this
    const args = [...arguments].slice(1)
    // 返回一個函數
    return function F() {
         // 由於返回了一個函數,咱們能夠 new F(),因此須要判斷
        if (this instanceof F){
            return new _this(...args,...arguments)
        }
        return _this.apply(context,
        args.concat(...arguments))
    }
}複製代碼

如下是對實現的分析:

  • 前幾步和以前的實現差很少,就不贅述了
  • bind 返回了一個函數,對於函數來講有兩種方式調用,一種是直接調用,一種是經過 new 的方式,咱們先來講直接調用的方式
  • 對於直接調用來講,這裏選擇了 apply 的方式實現,可是對於參數須要注意如下狀況:由於 bind 能夠實現相似這樣的代碼 f.bind(obj, 1)(2),因此咱們須要將兩邊的參數拼接起來,因而就有了這樣的實現 args.concat(...arguments)
  • 最後來講經過 new 的方式,在以前的章節中咱們學習過如何判斷 this,對於 new 的狀況來講,不會被任何方式改變 this,因此對於這種狀況咱們須要忽略傳入的 this

new

涉及面試題:new 的原理是什麼?經過 new 的方式建立對象和經過字面量建立有什麼區別?

在調用 new 的過程當中會發生以上四件事情:

1. 新生成了一個對象

2. 連接到原型

3. 綁定 this

4. 返回新對象

根據以上幾個過程,咱們也能夠試着來本身實現一個 new

function create() {
    let obj = {}
    let Con = [].shift.call(arguments)
    obj.__proto__ = Con.prototype
    let result = Con.apply(obj, arguments)
    return result instanceof Object ? result : obj
}複製代碼

如下是對實現的分析:

  • 建立一個空對象
  • 獲取構造函數
  • 設置空對象的原型
  • 綁定 this 並執行構造函數
  • 確保返回值爲對象

對於對象來講,其實都是經過 new 產生的,不管是 function Foo() 仍是 let a = { b : 1 } 。

對於建立一個對象來講,更推薦使用字面量的方式建立對象(不管性能上仍是可讀性)。由於你使用 new Object() 的方式建立對象須要經過做用域鏈一層層找到 Object,可是你使用字面量的方式就沒這個問題。

function Foo() {}
// function 就是個語法糖
// 內部等同於 new Function()
let a = { b: 1 }
// 這個字面量內部也是使用了 new Object()
更多關於 new 的內容能夠閱讀我寫的文章 聊聊 new 操做符。複製代碼

instanceof 的原理

涉及面試題:instanceof 的原理是什麼?

instanceof 能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 prototype。

咱們也能夠試着實現一下 instanceof

function myInstanceof(left, right) {
    let prototype = right.prototype
    left = left.__proto__
    while (true) {
        if (left === null || left === undefined)
        return false
        if (prototype === left)
        return true
        left = left.__proto__
    }
}複製代碼

如下是對實現的分析:

  • 首先獲取類型的原型
  • 而後得到對象的原型
  • 而後一直循環判斷對象的原型是否等於類型的原型,直到對象原型爲 null,由於原型鏈最終爲 null

爲何 0.1 + 0.2 != 0.3

涉及面試題:爲何 0.1 + 0.2 != 0.3?如何解決這個問題?

先說緣由,由於 JS 採用 IEEE 754 雙精度版本(64位),而且只要採用 IEEE 754 的語言都有該問題。

咱們都知道計算機是經過二進制來存儲東西的,那麼 0.1 在二進制中會表示爲

// (0011) 表示循環
0.1 = 2^-4 * 1.10011(0011)複製代碼

咱們能夠發現,0.1 在二進制中是無限循環的一些數字,其實不僅是 0.1,其實不少十進制小數用二進制表示都是無限循環的。這樣其實沒什麼問題,可是 JS 採用的浮點數標準卻會裁剪掉咱們的數字。

IEEE 754 雙精度版本(64位)將 64 位分爲了三段

  • 第一位用來表示符號
  • 接下去的 11 位用來表示指數
  • 其餘的位數用來表示有效位,也就是用二進制表示 0.1 中的 10011(0011)

那麼這些循環的數字被裁剪了,就會出現精度丟失的問題,也就形成了 0.1 再也不是 0.1 了,而是變成了 0.100000000000000002

0.100000000000000002 === 0.1 // true複製代碼

那麼一樣的,0.2 在二進制也是無限循環的,被裁剪後也失去了精度變成了 0.200000000000000002

0.200000000000000002 === 0.2 // true複製代碼

因此這二者相加不等於 0.3 而是 0.300000000000000004

0.1 + 0.2 === 0.30000000000000004 // true複製代碼

那麼可能你又會有一個疑問,既然 0.1 不是 0.1,那爲何 console.log(0.1) 倒是正確的呢?

由於在輸入內容的時候,二進制被轉換爲了十進制,十進制又被轉換爲了字符串,在這個轉換的過程當中發生了取近似值的過程,因此打印出來的實際上是一個近似值,你也能夠經過如下代碼來驗證

console.log(0.100000000000000002) // 0.1複製代碼

那麼說完了爲何,最後來講說怎麼解決這個問題吧。其實解決的辦法有不少,這裏咱們選用原生提供的方式來最簡單的解決問題

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true複製代碼

垃圾回收機制

涉及面試題:V8 下的垃圾回收機制是怎麼樣的?

V8 實現了準確式 GC,GC 算法採用了分代式垃圾回收機制。所以,V8 將內存(堆)分爲新生代和老生代兩部分。

新生代算法

新生代中的對象通常存活時間較短,使用 Scavenge GC 算法。

在新生代空間中,內存空間分爲兩部分,分別爲 From 空間和 To 空間。在這兩個空間中,一定有一個空間是使用的,另外一個空間是空閒的。新分配的對象會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啓動了。算法會檢查 From 空間中存活的對象並複製到 To 空間中,若是有失活的對象就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

老生代算法

老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。

在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:

  • 新生代中的對象是否已經經歷過一次 Scavenge 算法,若是經歷過的話,會將對象重新生代空間移到老生代空間中。
  • To 空間的對象佔比大小超過 25 %。在這種狀況下,爲了避免影響到內存分配,會將對象重新生代空間移到老生代空間中。

老生代中的空間很複雜,有以下幾個空間

enum AllocationSpace {
    // TODO(v8:7464): Actually map this space's memory as read-only. RO_SPACE, // 不變的對象空間 NEW_SPACE, // 新生代用於 GC 複製算法的空間 OLD_SPACE, // 老生代常駐對象空間 CODE_SPACE, // 老生代代碼對象空間 MAP_SPACE, // 老生代 map 對象 LO_SPACE, // 老生代大空間對象 NEW_LO_SPACE, // 新生代大空間對象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };複製代碼

在老生代中,如下狀況會先啓動標記清除算法:

  • 某一個空間沒有分塊的時候
  • 空間中被對象超過必定限制
  • 空間不能保證新生代中的對象移動到老生代中

在這個階段中,會遍歷堆中全部的對象,而後標記活的對象,在標記完成後,銷燬全部沒有被標記的對象。在標記大型對內存時,可能須要幾百毫秒才能完成一次標記。這就會致使一些性能上的問題。爲了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工做分解爲更小的模塊,可讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓狀況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名爲併發標記。該技術可讓 GC 掃描和標記對象時,同時容許 JS 運行,你能夠點擊 該博客 詳細閱讀。

清除對象後會形成堆內存出現碎片的狀況,當碎片超過必定限制後會啓動壓縮算法。在壓縮過程當中,將活的對象像一端移動,直到全部對象都移動完成而後清理掉不須要的內存。

小結

以上就是 JS 進階知識點的內容了,這部分的知識相比於以前的內容更加深刻也更加的理論,也是在面試中可以於別的候選者拉開差距的一塊內容。若是你們對於這個章節的內容存在疑問,歡迎在評論區與我互動。

JS 思考題

以前咱們經過了七個章節來學習關於 JS 這部分的內容,那麼接下來,會以幾道思考題的方式來確保你們理解這部分的內容。

這種方式不只能加深你對知識點的理解,同時也能幫助你串聯起多個碎片知識點。一旦你擁有將多個碎片知識點串聯起來的能力,在面試中就不會常常出現一問一答的狀況。若是面試官的每一個問題你都能引伸出一些相關聯的知識點,那麼面試官必定會提升對你的評價。

思考題一:JS 分爲哪兩大類型?都有什麼各自的特色?你該如何判斷正確的類型?

首先這幾道題目想必不少人都可以很好的答出來,接下來就給你們一點思路講出不同凡響的東西。

思路引導:

1. 對於原始類型來講,你能夠指出 null 和 number 存在的一些問題。對於對象類型來講,你能夠從垃圾回收的角度去切入,也能夠說一下對象類型存在深淺拷貝的問題。

2. 對於判斷類型來講,你能夠去對比一下 typeof 和 instanceof 之間的區別,也能夠指出 instanceof 判斷類型也不是徹底準確的。

以上就是這道題目的回答思路,固然不是說讓你們徹底按照這個思路去答題,而是存在一個意識,當回答面試題的時候,儘可能去引伸出這個知識點的某些坑或者與這個知識點相關聯的東西。

思考題二:你理解的原型是什麼?

思路引導:

起碼說出原型小節中的總結內容,而後還能夠指出一些小點,好比並非全部函數都有 prototype 屬性,而後引伸出原型鏈的概念,提出如何使用原型實現繼承,繼而能夠引伸出 ES6 中的 class 實現繼承。

思考題三:bind、call 和 apply 各自有什麼區別?

思路引導:

首先確定是說出三者的不一樣,若是本身實現過其中的函數,能夠嘗試說出本身的思路。而後能夠聊一聊 this 的內容,有幾種規則判斷 this 究竟是什麼,this 規則會涉及到 new,那麼最後能夠說下本身對於 new 的理解。

思考題四:ES6 中有使用過什麼?

思路引導:

這邊可說的實在太多,你能夠列舉 1 - 2 個點。好比說說 class,那麼 class 又能夠拉回到原型的問題;能夠說說 promise,那麼線就被拉到了異步的內容;能夠說說 proxy,那麼若是你使用過 Vue 這個框架,就能夠談談響應式原理的內容;一樣也能夠說說 let 這些聲明變量的語法,那麼就能夠談及與 var 的不一樣,說到提高這塊的內容。

思考題五:JS 是如何運行的?

思路引導:

這實際上是很大的一塊內容。你能夠先說 JS 是單線程運行的,這裏就能夠說說你理解的線程和進程的區別。而後講到執行棧,接下來的內容就是涉及 Eventloop 了,微任務和宏任務的區別,哪些是微任務,哪些又是宏任務,還能夠談及瀏覽器和 Node 中的 Eventloop 的不一樣,最後還能夠聊一聊 JS 中的垃圾回收。

小結

雖然思考題很少,可是其實每一道思考題背後均可以引伸出不少內容,你們接下去在學習的過程當中也應該始終有一個意識,你學習的這塊內容到底和你如今腦海裏的哪個知識點有關聯。同時也歡迎你們總結這些思考題,而且把總結的內容連接放在評論中,我會挑選出不錯的文章單獨放入一章節給你們參考。

DOM 斷點

給 JS 打斷點想必各位都聽過,可是 DOM 斷點知道的人應該就少了。若是你想查看一個 DOM 元素是如何經過 JS 更改的,你就可使用這個功能。

當咱們給 ul 添加該斷點之後,一旦 ul 子元素髮生了改動,好比說增長了子元素的個數,那麼就會自動跳轉到對應的 JS 代碼

其實不光能夠給 DOM 打斷點,咱們還能夠給 Ajax 或者 Event Listener 打斷點。

查看事件

咱們還能夠經過 DevTools 來查看頁面中添加了多少的事件。假如當你發現頁面滾動起來有性能上的問題時,就能夠查看一下有多少 scroll 事件被添加了

找到以前查看過的 DOM 元素

不知道你是否遇到過這樣的問題,找不到以前查看過的 DOM 元素在哪裏了,須要一個個去找這就有點麻煩了,這時候你就可使用這個功能。

咱們能夠經過 $0 來找到上一次查看過的 DOM 元素,$1 就是上上次的元素,以後以此類推。這時候你可能會說,打印出來元素有啥用,在具體什麼位置還要去找啊,不用急,立刻我就能夠解決這個問題

當你點擊這個選項時,頁面立馬會跳轉至元素所在位置,而且 DevTools 也會變到 Elements 標籤。

Debugging

給 JS 打斷點想必你們都會,可是打斷點也是有一個鮮爲人知的 Tips 的。

for (let index = 0; index < 10; index++) {
    // 各類邏輯
    console.log(index)
}複製代碼

對於這段代碼來講,若是我只想看到 index 爲 5 時相應的斷點信息,可是一旦打了斷點,就會每次循環都會停下來,很浪費時間,那麼經過這個小技巧咱們就能夠圓滿解決這個問題


首先咱們先右鍵斷點,而後選擇 Edit breakpoint... 選項

在彈框內輸入 index === 5,這樣斷點就會變爲橙色,而且只有當符合表達式的狀況時斷點纔會被執行

小結

雖然這一章的內容並很少,可是涉及到的幾個場景都是平常常常會碰到的,但願這一章節的內容會對你們有幫助。

相關文章
相關標籤/搜索