帶你理解 JS 容易出錯的坑和細節

目前本身組建的一個團隊正在寫一份面試圖譜,將會在七月中旬開源。內容十分豐富,初版會開源前端方面知識和程序員必備知識,後期會逐步寫入後端方面知識。由於工程所涉及內容太多(目前已經寫了一個半月),而且還需翻譯成英文,因此所需時間較長。有興趣的同窗能夠 Follow 個人 Github 獲得最快的更新消息。javascript

執行環境(Execution context)

var 和 let 的正確解釋

當執行 JS 代碼時,會生成執行環境,只要代碼不是寫在函數中的,就是在全局執行環境中,函數中的代碼會產生函數執行環境,只此兩種執行環境。前端

接下來讓咱們看一個老生常談的例子,varjava

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}
複製代碼

想必以上的輸出你們確定都已經明白了,這是由於函數和變量提高的緣由。一般提高的解釋是說將聲明的代碼移動到了頂部,這其實沒有什麼錯誤,便於你們理解。可是更準確的解釋應該是:在生成執行環境時,會有兩個階段。第一個階段是建立的階段,JS 解釋器會找出須要提高的變量和函數,而且給他們提早在內存中開闢好空間,函數的話會將整個函數存入內存中,變量只聲明而且賦值爲 undefined,因此在第二個階段,也就是代碼執行階段,咱們能夠直接提早使用。git

在提高的過程當中,相同的函數會覆蓋上一個函數,而且函數優先於變量提高程序員

b() // call b second

function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'
複製代碼

var 會產生不少錯誤,因此在 ES6中引入了 letlet 不能在聲明前使用,可是這並非常說的 let 不會提高,let 提高了,在第一階段內存也已經爲他開闢好了空間,可是由於這個聲明的特性致使了並不能在聲明前使用。github

做用域

function b() {
    console.log(value)
}

function a() {
    var value = 2
    b()
}

var value = 1
a()
複製代碼

能夠考慮下 b 函數中輸出什麼。你是否會認爲 b 函數是在 a 函數中調用的,相應的 b 函數中沒有聲明 value 那麼應該去 a 函數中尋找。其實答案應該是 1。面試

當在產生執行環境的第一階段時,會生成 [[Scope]] 屬性,這個屬性是一個指針,對應的有一個做用域鏈表,JS 會經過這個鏈表來尋找變量直到全局環境。這個指針指向的上一個節點就是該函數聲明的位置,由於 b 是在全局環境中聲明的,因此 value 的聲明會在全局環境下尋找。若是 b 是在 a 中聲明的,那麼 log 出來的值就是 2 了。後端

異步

JS 是門同步的語言,你是否疑惑過那麼爲何 JS 還有異步的寫法。其實 JS 的異步和其餘語言的異步是不相同的,本質上仍是同步。由於瀏覽器會有多個 Queue 存放異步通知,而且每一個 Queue 的優先級也不一樣,JS 在執行代碼時會產生一個執行棧,同步的代碼在執行棧中,異步的在 Queue 中。有一個 Event Loop 會循環檢查執行棧是否爲空,爲空時會在 Queue 中查看是否有須要處理的通知,有的話拿到執行棧中去執行。數組

function sleep() {
  var ms = 2000 + new Date().getTime()
  while( new Date() < ms) {}
  console.log('sleep finish')
}

document.addEventListener('click', function() {
  console.log('click')
})

sleep()
setTimeout(function() {
    console.log('timeout');
}, 0);

Promise.resolve().then(function() {
    console.log('promise');
});
console.log('finish')
複製代碼

以上代碼若是你在 sleep 被調用期間點擊,只有當 sleep 執行結束而且 log finish 後纔會響應其餘異步事件。因此要注意 setTimeout 並非你設定多久 JS 就會準時的響應,而且 setTimeout 也有個小細節,第二個參數設置爲 0 也許會有人認爲這樣就不是異步了,其實仍是異步。這是由於 HTML5 標準規定這個函數第二個參數不得小於 4 毫秒,不足會自動增長。promise

如下輸出創建在 Chrome 上,不一樣的瀏覽器會有不一樣的輸出

promise // promise 會進入 Microtask Queue 中,這個 Queue 會優先執行
timeout // setTimeout 會進入 task Queue 中
click // 點擊事件會進入 Event Queue 中
複製代碼

類型

原始值

JS 共有 6 個原始值,分別爲 Boolean, Null, Undefined, Number, String, Symbol,這些類型都是值不可變的。

有一個易錯的點是:雖然 typeof null 是 object 類型,可是 Null 不是對象,這是 JS 語言的一個好久遠的 Bug 了。

深淺拷貝

對於對象來講,直接將一個對象賦值給另一個對象就是淺拷貝,兩個對象指向同一個地址,其中任何一個對象改變,另外一個對象也會被改變

var a = [1, 2]
var b = a
b.push(3)
console.log(a, b) // -> 都是 [1, 2, 3]
複製代碼

有些狀況下咱們可能不但願有這種問題,那麼深拷貝能夠解決這個問題。深拷貝不只將原對象的各個屬性逐個複製出去,並且將原對象各個屬性所包含的對象也依次採用深複製的方法遞歸複製到新對象上。深拷貝有多種寫法,有興趣的能夠看這篇文章

函數和對象

this

this 是不少人會混淆的概念,可是其實他一點都不難,你只須要記住幾個規則就能夠了。

function foo() {
  console.log(this.a)
}
var a = 2
foo() 

var obj = {
  a: 2,
  foo: foo
}
obj.foo() 

// 以上二者狀況 this 只依賴於調用函數前的對象,優先級是第二個狀況大於第一個狀況

// 如下狀況是優先級最高的,this 只會綁定在 c 上
var c = new foo()
c.a = 3
console.log(c.a)

// 還有種就是利用 call,apply,bind 改變 this,這個優先級僅次於 new
複製代碼

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

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

箭頭函數實際上是沒有 this 的,這個函數中的 this 只取決於他外面的第一個不是箭頭函數的函數的 this。在這個例子中,由於調用 a 符合前面代碼中的第一個狀況,因此 this 是 window。而且 this 一旦綁定了上下文,就不會被任何代碼改變。

下面咱們再來看一個例子,不少人認爲他是一個 JS 的問題

var a = {
    name: 'js',
    log: function() {
        console.log(this)
        function setName() {
            this.name = 'javaScript'
            console.log(this)
        }
        setName()
    }
}
a.log()
複製代碼

setName 中的 this 指向了 window,不少人認爲他應該是指向 a 的。這裏其實咱們不須要去管函數是寫在什麼地方的,咱們只須要考慮函數是怎麼調用的,這裏符合上述第一個狀況,因此應該是指向 window。

閉包和當即執行函數

閉包被不少人認爲是一個很難理解的概念。其實閉包很簡單,就是一個可以訪問父函數局部變量的函數,父函數在執行完後,內部的變量還存在內存上讓閉包使用。

function a(name) {
    // 這就是閉包,由於他使用了父函數的參數
    return function() {
        console.log(name)
    }
}
var b = a('js')
b() // -> js
複製代碼

如今來看一個面試題

function a() {
    var array = []

    for(var i = 0; i < 3; i++) {
        array.push(
            function() {
                console.log(i)
            }
        )
    }

    return array
}

var b = a()
b[0]()
b[1]()
b[2]()
複製代碼

這個題目由於 i 被提高了,因此 i = 3,當 a 函數執行完成後,內存中保留了 a 函數中的變量 i。數組中 push 進去的只是聲明,並無執行函數。因此在執行函數時,輸出了 3 個 3。

若是咱們想輸出 0 ,1,2 的話,有兩種簡單的辦法。第一個是在 for 循環中,使用 let 聲明一個變量,保存每次的 i 值,這樣在 a 函數執行完成後,內存中就保存了 3 個不一樣 let 聲明的變量,這樣就解決了問題。

還有個辦法就是使用當即執行函數,建立函數即執行,這樣就能夠保存下當前的 i 的值。

function a() {
    var array = []

    for(var i = 0; i < 3; i++) {
        array.push(
            (function(j) {
                return function() {
                    console.log(j)
                }
            }(i))
        )
    }

    return array
}
複製代碼

當即執行函數其實就是直接調用匿名函數

function() {} ()
複製代碼

可是以上寫法會報錯,由於解釋器認爲這是一個函數聲明,不能直接調用,因此咱們加上了一個括號來讓解釋器認爲這是一個函數表達式,這樣就能夠直接調用了。

因此咱們其實只須要讓解釋器認爲咱們寫了個函數表達式就好了,其實還有不少種當即執行函數寫法

true && function() {} ()
new && function() {} ()
複製代碼

當即執行函數最大的做用就是模塊化,其次就是解決上述閉包的問題了。

原型,原型鏈和 instanceof 原理

原型可能不少人以爲很複雜,本章節也不打算重複複述不少文章都講過的概念,你只須要看懂我畫的圖而且本身實驗下便可

function P() {
    console.log('object')
}

var p = new P()

複製代碼

原型鏈就是按照 __proto__ 尋找,直到 Object。instanceof 原理也是根據原型鏈判斷的

p instanceof P // true
p instanceof Object // true
複製代碼

相關文章
相關標籤/搜索