你不知道的JavaScript小結匯總

你不知道的JavaScript(上卷)

什麼是作用域?

作用域是一套規則,用於確定在何處以及如何查找變量(標識符)。如果查找的目的是對變量進行賦值,那麼就會使用LHS查詢,如果目的是獲取變量的值,就會使用RHS查詢。

賦值操作會導致LHS查詢。=操作符或調用函數時傳入的參數都會導致關聯作用域的賦值操作

JavaScript引擎會在代碼執行前對其進行編譯,在這個過程中,像 var = 2 這樣的聲明會被分解成兩個步驟:
首先,var a 在其作用域中聲明變量,這會在最開始的階段,也就是代碼執行前進行(預解析)
接下來,a=2 會查詢(LHS查詢)變量a並對其進行賦值。

LHS和RHS查詢都會從當前執行作用域中開始,如果有需要(也就是說他們沒有找到所需的標識符),就會向上級作用域查找目標標識符,這樣每次上升一級作用域(一層樓),最後抵達全局作用域(頂層),無論找到或者沒找到都會停止查找。

不成功的RHS查詢會導致拋出ReferenceError異常。不成功的LHS查詢會導致自動隱式創建一個全局變量(非嚴格模式下),
該變量會使用LHS引用的目標作爲標識符,或者拋出ReferenceError異常(嚴格模式下)。

//小測試
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
  1. 找出所有的 LHS 查詢(這裏有 3 處!)
    c = foo(2); a = 2(隱式變量分配)、b = a
  2. 找出所有的 RHS 查詢(這裏有 4 處!)
    =foo(2); = a;、a + b(這裏是兩次)

詞法作用域

詞法作用域意味着作用域是由書寫代碼時函數聲明的位置來決定的。編譯的詞法分析階段(預解析階段)基本能夠知道全部標識符在哪裏以及如何聲明的,從而能夠預測在執行過程中如何對他們進行查找。

JavaScript中有兩個機制可以“欺騙”詞法作用域:eval(…)和with。前者可以對一段包含一個或多個聲明的“代碼”字符串進行演算,並以此來修改已經存在的詞法作用域(在運行時) 。後者本質上是通過將一個對象的引用當做作用域來處理,將對象的屬性當做作用域中的標識符來處理,從而創建了一個新的詞法作用域(同樣是在運行時)。

這兩個機制的副作用是引擎無法在編譯時對作用域查找進行優化,因爲引擎只能謹慎的認爲這樣的優化是無效的,使用這其中任何一個機制都將導致代碼運行變慢,不要使用他們。

函數作用域和塊作用域

函數是JavaScript中最常見的作用域單元。本質上,聲明在函數內部的變量或函數會在所處的作用域中“隱藏”起來,這是有意而爲之的良好軟件的設計原則。

但函數不是唯一的作用域單元。塊級作用域指的是變量和函數不僅可以屬於所處的作用域,也可以屬於某個代碼塊(通常指{…}內部)。

從ES3開始,try/catch結構在catch分句中具有塊作用域。

在ES6中引入了let關鍵字(var 關鍵字的表親),用來在任意代碼塊中聲明變量。if(…){let a = 2;}會聲明一個劫持了if的{…}塊的變量,並且將這個變量添加到這個塊中。

有些人認爲塊作用域不應該完全作爲函數作用域的替代方案。這兩種功能應該同時存在,開發者可以並且也應該根據需要選擇使用何種作用域,創造可讀、可維護的優良代碼。

提升

我們習慣將 var a = 2; 看做是一個聲明,而實際上JavaScript引擎並不這麼認爲。他將 var a 和 a = 2 當做是兩個單獨的聲明,第一個是編譯階段的任務,而第二個則是執行階段的任務。

這意味着無論作用域中的聲明出現在什麼地方,都將在代碼本身被執行前首先進行處理。
可以將這個過程形象的想象成所有的聲明(變量和函數)都會被“移動”到各自作用域的最頂端,這個過程被稱爲提升。

聲明本身會被提升,而包括函數表達式的賦值在內的賦值操作並不會提升。

要注意避免重複聲明,特別是當普通的var聲明和函數聲明混合在一起的時候,否則會引起很多危險的問題!

作用域閉包

閉包就好像從JavaScript中分離出來的一個充滿神祕色彩的的未開化的世界,只有最勇敢的人才能夠到達那裏。但實際上他只是一個標準,顯然就是關於如何在函數作爲值按需傳遞的詞法環境中書寫代碼的。

(*)當函數可以記住並訪問所在的詞法作用域,即使函數是在當前詞法作用域之外執行,這時就產生了閉包

如果沒能認出閉包,也不瞭解他的工作原理,在使用它的過程中就很容易犯錯,比如在循環中。但同時閉包也是一個非常強大的工具,可以用多種形式來實現模塊等模式。

模塊有兩個主要特徵:
(1)爲創建內部作用域而調用了一個包裝函數;
(2)包裝函數的返回值必須至少包括一個內部函數的引用,這樣就會創建涵蓋整個包裝函數內部作用域的閉包。

現在我們會發現代碼中到處都有閉包存在,並且我們能夠識別閉包然後用他來做一些有用的事!

第二部分 this和原型

this和對象原型

對於那些那些沒有投入時間學習this機制的JavaScript開發者來說,this的綁定一定一直是意見非常令人困惑的事。this是非常重要的,但是猜測,嘗試並出錯和盲目的從Stack Overflow上覆制和粘貼答案並不能讓你真正理解this的機制

想學習this的第一步是明白this既不指向函數自身也不指向函數的詞法作用域,你也許被斜眼的解釋舞蹈過,但其實他們都是錯誤的。

this實際上是在函數被調用時發生的綁定,他指向什麼完全取決於函數在哪裏被調用。

this全面解析

如果要判斷一個運行中函數的this綁定,就需要找到這個函數的直接調用位置,找到之後就可以順序應用下面這四條規則來判斷this的綁定對象。

1.由new調用?綁定到新創建的對象。
2.由call或者apply(或者bind)調用?綁定到指定的對象。
3.由上下文調用?綁定到那個上下文對象。
4.默認:在嚴格模式下綁定到undefined,否則綁定到全局對象。

一定要注意,有些調用可能在無意中使用默認綁定規則。如果想“更安全”的忽略this綁定,你可以使用一個DMZ對象,比如$ = Object.create(null),以保護全局對象

ES6中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法作用域來決定this,具體來說,箭頭函數會繼承外層函數調用的this綁定(無論this綁定到什麼)。這其實和ES6之前代碼中self = this 機制一樣

對象

許多人都以爲“JavaScript中萬物都是對象”,這是錯誤的,對象是6個(或者是7個,取決於你的觀點)基礎類型之一。對象有包括function在內的子類型,不同子類型具有不同的行爲,比如內部標籤[object Array]表示這是對象的子類型數組。

對象就是鍵/值對的集合。可以通過.propName或者[“propName”]語法來獲取屬性值。訪問屬性時,引擎實際上會調用內部的[[Get]]操作(在設置屬性時是[[Put]]),[[Get]]操作會檢查對象本身是否包含這個屬性,如果沒找的的話還會查找[[Prototype]]鏈。

屬性的特性可以通過屬性描述符來控制,比如writable和configurable。此外,可以使用Object.preventExtensions(…)、Object.seal(…)、和Object.freeze(…)來設置對象(及其屬性的不可變性級別)。

屬性不一定包含值——他們可能是具備getter/setter的“訪問描述符”。此外,屬性可以是可枚舉和不可枚舉的,這決定了它是否會出現在for…in循環中。

你可以使用ES6的for…of語法來遍歷數據結構(數組,對象,等等)中的值,for…of會尋找內置或者自定義的@@iterator對象並調用她的next()方法來遍歷數據值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章