前段時間在天天往返的地鐵上抽空將 《你不知道的JavaScript(上卷)》讀了一遍,這本書不少部分寫的非常精妙,對於接觸前端時間不過久的人來講,就好像是叩開了JavaScript的另外一扇門,不少內容醍醐灌頂!因此決定將這本書分四個部分整理出來,同時也這本書強烈推薦給正在進階的小夥伴們。這篇博文主要整理第一部分 做用域。前端
首先要介紹下JS參與程序 var a = 2的處理過程的演員表:安全
引擎函數
從頭至尾負責整個JavaScript 程序的編譯及執行過程。性能
編譯器this
引擎的好朋友之一,負責語法分析及代碼生成等髒活累活調試
做用域code
引擎的另外一位好朋友,負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。對象
在處理過程當中,引擎會爲變量a 進行LHS 查詢。另一個查找的類型叫做RHS。當變量出如今賦值操做的左側時進行LHS 查詢,出如今右側時進行RHS 查詢。遞歸
console.log(a) // RHS查詢
function foo(a) { console.log( a ); // 2 } foo( 2 ); 引擎:我說做用域,我須要爲foo 進行RHS 引用。你見過它嗎? 做用域:別說,我還真見過,編譯器那小子剛剛聲明瞭它。它是一個函數,給你。 引擎:哥們太夠意思了!好吧,我來執行一下foo。 引擎:做用域,還有個事兒。我須要爲a 進行LHS 引用,這個你見過嗎? 做用域:這個也見過,編譯器最近把它聲名爲foo 的一個形式參數了,拿去吧。 引擎:大恩不言謝,你老是這麼棒。如今我要把2 賦值給a。 引擎:哥們,很差意思又來打擾你。我要爲console 進行RHS 引用,你見過它嗎? 做用域:咱倆誰跟誰啊,再說我就是幹這個。這個我也有,console 是個內置對象。 給你。 引擎:麼麼噠。我得看看這裏面是否是有log(..)。太好了,找到了,是一個函數。 引擎:哥們,能幫我再找一下對a 的RHS 引用嗎?雖然我記得它,但想再確認一次。 做用域:放心吧,這個變量沒有變更過,拿走,不謝。 引擎:真棒。我來把a 的值,也就是2,傳遞進log(..)。
當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量,或抵達最外層的做用域(也就是全局做用域)爲止。事件
若是RHS 查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出ReferenceError異常。值得注意的是,ReferenceError 是很是重要的異常類型。
若是RHS 查詢找到了一個變量,可是你嘗試對這個變量的值進行不合理的操做,好比試圖對一個非函數類型的值進行函數調用,或着引用null 或undefined 類型的值中的屬性,那麼引擎會拋出另一種類型的異常,叫做TypeError。
ReferenceError 同做用域判別失敗相關,而TypeError 則表明做用域判別成功了,可是對結果的操做是非法或不合理的。
大部分標準語言編譯器的第一個工做階段叫做詞法化,詞法做用域就是定義在詞法階段的做用域.
function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar( b * 3 ); } foo( 2 ); // 2, 4, 12
上面例子中,有三個嵌套的做用域:
做用域查找會在找到第一個匹配的標識符時中止
有些函數會在運行時修改詞法做用域,可是欺騙詞法做用域會致使性能降低。
在執行eval(..) 以後的代碼時,引擎並不「知道」或「在乎」前面的代碼是以動態形式插入進來,並對詞法做用域的環境進行修改的。引擎只會如往常地進行詞法做用域查找。
function foo(str, a) { eval( str ); // 欺騙! console.log( a, b ); } var b = 2; foo( "var b = 3;", 1 ); // 1, 3
eval(..) 調用中的"var b = 3;" 這段代碼會被看成原本就在那裏同樣來處理。因爲那段代碼聲明瞭一個新的變量b,所以它對已經存在的foo(..) 的詞法做用域進行了修改。事實上,和前面提到的原理同樣,這段代碼實際上在foo(..) 內部建立了一個變量b,並遮蔽了外部(全局)做用域中的同名變量。可是在嚴格模式下,eval(..) 在運行時有其本身的詞法做用域,意味着其中的聲明沒法修改所在的做用域。
with
function foo(obj) { with (obj) { a = 2; } } var o2 = { b: 3 }; foo( o2 ); console.log( o2.a ); // undefined console.log( a ); // 2——很差,a 被泄漏到全局做用域上了!
當咱們將o2 做爲做用域時,其中並無a 標識符,所以進行了正常的LHS 標識符查找。o2 的做用域、foo(..) 的做用域和全局做用域中都沒有找到標識符a,所以當a=2 執行時,自動建立了一個全局變量(由於是非嚴格模式)。
eval(..) 函數若是接受了含有一個或多個聲明的代碼,就會修改其所處的詞法做用域,而with 聲明其實是根據你傳遞給它的對象憑空建立了一個全新的詞法做用域。不推薦使用eval(..) 和with 的緣由是會被嚴格模式所影響(限制)。with 被徹底禁止,而在保留核心功能的前提下,間接或非安全地使用eval(..) 也被禁止了。
在任意代碼片斷外部添加包裝函數,能夠將內部的變量和函數定義「隱藏」起來,外部做用域沒法訪問包裝函數內部的任何內容。
函數聲明與函數表達式
區分函數聲明和表達式最簡單的方法是看function 關鍵字出如今聲明中的位置(不只僅是一行代碼,而是整個聲明中的位置)。若是function 是聲明中的第一個詞,那麼就是一個函數聲明,不然就是一個函數表達式。
函數聲明和函數表達式之間最重要的區別是它們的名稱標識符將會綁定在何處。
匿名與具名
setTimeout( function() { console.log("I waited 1 second!"); }, 1000 );
這叫做匿名函數表達式,由於function().. 沒有名稱標識符。函數表達式能夠是匿名的,而函數聲明則不能夠省略函數名——在JavaScript 的語法中這是非法的。
匿名函數的弊端:
匿名函數省略了對於代碼可讀性/ 可理解性很重要的函數名。一個描述性的名稱可讓代碼不言自明。
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了! console.log( "I waited 1 second!" ); }, 1000 );
當即執行函數表達式(IIFE)
var a = 2; (function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 })( window ); console.log( a ); // 2
塊級做用域
for (var i=0; i<10; i++) { console.log( i ); } // 爲何要把一個只在for 循環內部使用(至少是應該只在內部使用)的變量i 污染到整個函數做用域中呢?
經常使用的塊級做用域:
try/catch
try { undefined(); // 執行一個非法操做來強制製造一個異常 } catch (err) { console.log( err ); // 可以正常執行! } console.log( err ); // ReferenceError: err not found
let/const (這兩爲ES6最基本的關鍵字,就很少介紹了,可是很重要!)
先有雞仍是先有蛋
a = 2; var a; console.log( a );
上面代碼你認爲會輸出什麼?不少開發者會認爲是undefined,由於var a 聲明在a = 2 以後,他們天然而然地認爲變量被從新賦值了,所以會被賦予默認值undefined。可是,真正的輸出結果是2。包括變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理。
當你看到var a = 2; 時,可能會認爲這是一個聲明。但JavaScript 實際上會將其當作兩個聲明:var a; 和a = 2;。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執行階段。
剛纔的代碼會被處理爲:
var a; //提高 a = 2; console.log( a );
函數優先
函數聲明和變量聲明都會被提高。可是一個值得注意的細節(這個細節能夠出如今有多個「重複」聲明的代碼中)是函數會首先被提高,而後纔是變量。考慮如下代碼:
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
會輸出1 而不是2 !這個代碼片斷會被引擎理解爲以下形式:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
最後提一點:js只有詞法做用域,無動態做用域。可是this 機制某種程度上很像動態做用域。他們主要區別:詞法做用域是在寫代碼或者說定義時肯定的,而動態做用域是在運行時肯定的。(this 也是!)詞法做用域關注函數在何處聲明,而動態做用域關注函數從何處調用。