1. 程式人生 > >JavaScript 常見大坑與細節

JavaScript 常見大坑與細節

直接 階段 settime all 上一個 成績 系統分享 常見 內容

執行環境(Execution context)

var 和 let 的正確解釋

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

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

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

var a = ‘Hello world‘

function b() {
    console.log(‘call b‘)
}

想必以上的輸出大家肯定都已經明白了,這是因為函數和變量提升的原因。通常提升的解釋是說將聲明的代碼移動到了頂部,這其實沒有什麽錯誤,便於大家理解。但是更準確的解釋應該是:在生成執行環境時,會有兩個階段。第一個階段是創建的階段,JS 解釋器會找出需要提升的變量和函數,並且給他們提前在內存中開辟好空間,函數的話會將整個函數存入內存中,變量只聲明並且賦值為 undefined,所以在第二個階段,也就是代碼執行階段,我們可以直接提前使用。

在提升的過程中,相同的函數會覆蓋上一個函數,並且函數優先於變量提升

b() // call b second

function b() {
    console.log(‘call b fist‘)
}
function b() {
    console.log(‘call b second‘)
}
var b = ‘Hello world‘

var 會產生很多錯誤,所以在 ES6中引入了 let。let 不能在聲明前使用,但是這並不是常說的 let 不會提升,let 提升了,在第一階段內存也已經為他開辟好了空間,但是因為這個聲明的特性導致了並不能在聲明前使用。

作用域

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 毫秒,不足會自動增加。

以下輸出建立在 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

很多時候跟著書和網站查找資料學習,會發現沒有目標,學了很多卻不知道自己到底能夠做出什麽成績。要有一個清晰的職業學習規劃,學習過程中會遇到很多問題,你可以到我們的前端學習交流Q—q-u-n【 731771211 】,基礎,進階。從企業招聘人才需求 到怎麽學習前端開發,和學習什麽內容都有免費系統分享,讓你無論是自學還是找相應的培訓都能讓你少走彎路。希望可以幫助你快速了解前端,學習前端

自己從事前端開發五年了,希望能幫助大家更好的學習前端

點擊:加入

JavaScript 常見大坑與細節