看了《你不知道的JavaScript(上卷)》的第一部分——做用域和閉包,感覺頗深,遂寫一篇讀書筆記加深印象。路過的大牛歡迎指點,對這方面不懂的同窗請繞道看書,以避免誤人子弟... 看過這本書的能夠一塊兒交流交流。javascript
理解js做用域首先要了解js的編譯過程(或者說解析過程)。java
引擎node
從頭至尾負責整個 JavaScript 程序的編譯及執行過程。編譯器chrome
引擎的好朋友之一,負責語法分析及代碼生成等髒活累活(詳見前一節的內容)。做用域性能優化
引擎的另外一位好朋友,負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。
都說node是基於chrome的V8引擎開發 的。那麼V8是引擎,node是編譯器嗎?這個理解是錯誤的!我以前就是這麼錯誤理解的,據說node是用C++實現的,以前我一直覺得V8是負責把javascript語言轉換成底層的C++,而後node很高級node負責編譯,作js的語法檢察,ES6的新特性全都是node的開發人員,一點點的開發支持起來的。然而現實是,V8包辦了全部js編譯的過程,而node只是一個環境。如nodejs.cn首頁所說Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。
,是運行環境!node只是在V8的基礎上,作了終端命令行的支持、文件處理的支持、http服務的支持等等,至關於一個給V8提供了各類功能的殼子。閉包
上面說的三點是包含關係,不是並行關係!引擎包含編譯器,對js進行編譯,而後根據做用域和語句執行不一樣的代碼邏輯。函數
咱們將 var a = 2; 分解,看看引擎和它的朋友們是如何協同工做的。性能
編譯器首先會將這段程序分解成詞法單元,而後將詞法單元解析成一個樹結構。可是當編 譯器開始進行代碼生成時,它對這段程序的處理方式會和預期的有所不一樣。
能夠合理地假設編譯器所產生的代碼可以用下面的僞代碼進行歸納:「爲一個變量分配內 存,將其命名爲 a,而後將值 2 保存進這個變量。」然而,這並不徹底正確。
事實上編譯器會進行以下處理。測試
1. 遇到var a,編譯器會詢問做用域是否已經有一個該名稱的變量存在於同一個做用域的 集合中。若是是,編譯器會忽略該聲明,繼續進行編譯;不然它會要求做用域在當前做 用域的集合中聲明一個新的變量,並命名爲 a。 2. 接下來編譯器會爲引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值 操做。引擎運行時會首先詢問做用域,在當前的做用域集合中是否存在一個叫做 a 的 變量。若是是,引擎就會使用這個變量;若是否,引擎會繼續查找該變量。
若是引擎最終找到了 a 變量,就會將 2 賦值給它。不然引擎就會舉手示意並拋出一個異常!優化
在咱們的例子中,引擎會爲變量 a 進行 LHS 查詢。另一個查找的類型叫做 RHS。
RHS 查詢與簡單地查找某個變量的值別無二致,而 LHS 查詢則是試圖 找到變量的容器自己,從而能夠對其賦值。從這個角度說,RHS 並非真正意義上的「賦 值操做的右側」,更準確地說是「非左側」。
你能夠將RHS理解成retrieve his source value(取到它的源值),這意味着「獲得某某的 值」。
怎麼理解呢,個人理解是LHS 查詢是查詢變量的命名空間,而後進行賦值。RHS 查詢是在做用域鏈中,一級級的往上查找該變量的引用。
因此:
function foo(a) { var b=a; return a + b; } var c=foo(2);
找到其中全部的LHS查詢。(這裏有3處!)
找到其中全部的RHS查詢。(這裏有4處!)
LHS:var c=
的賦值、foo(2)
傳參給foo(a)
時的賦值、var b=
的賦值
RHS:foo(2)
函數調用時查找foo()方法、var b=a
中a查找本身的值、a+b
中a和b兩個參數查找本身的值。
做用域的概念,應該兩張圖幾句話就能解釋吧。
這個建築表明程序中的嵌套做用域鏈。第一層樓表明當前的執行做用域,也就是你所處的 位置。建築的頂層表明全局做用域。
LHS 和 RHS 引用都會在當前樓層進行查找,若是沒有找到,就會坐電梯前往上一層樓, 若是仍是沒有找到就繼續向上,以此類推。一旦抵達頂層(全局做用域),可能找到了你 所需的變量,也可能沒找到,但不管如何查找過程都將中止。
① 包含着整個全局做用域,其中只有一個標識符:foo。
② 包含着 foo 所建立的做用域,其中有三個標識符:a、bar 和 b。
③ 包含着 bar 所建立的做用域,其中只有一個標識符:c。
做用域氣泡由其對應的做用域塊代碼寫在哪裏決定,它們是逐級包含的。
我以爲,說一個變量屬於哪一個做用域,能夠顧名思義用該變量生效的區域來解釋,因此上圖中的b變量,能夠說屬於bar()的函數做用域內,也能夠說是foo()的函數做用域內,也能夠說是全局做用域內。
一層嵌一層的做用域造成了做用域鏈
,變量b在做用域鏈中的foo()函數內獲得了本身的定義。
eval(..) 和 with 會在運行時修改或建立新的做用域,以此來欺騙其餘在書寫時定義的詞法做用域。
JavaScript 引擎會在編譯階段進行數項的性能優化。其中有些優化依賴於可以根據代碼的 詞法進行靜態分析,並預先肯定全部變量和函數的定義位置,才能在執行過程當中快速找到 標識符。
但若是引擎在代碼中發現了 eval(..) 或 with,它只能簡單地假設關於標識符位置的判斷 都是無效的,由於沒法在詞法分析階段明確知道 eval(..) 會接收到什麼代碼,這些代碼會 如何對做用域進行修改,也沒法知道傳遞給 with 用來建立新詞法做用域的對象的內容到底 是什麼。這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎地認 爲這樣的優化是無效的。使用這其中任何一個機制都將致使代碼運行變慢。不要使用它們。
call()
、bind()
之類的是改變做用域嗎?他們只是改變了this的指向並不算改變做用域,是能夠在編譯階段進行靜態分析,因此不會致使上面說的沒法優化的狀況。
咱們知道函數能夠造成做用域,還有哪些方式造成做用域呢?
能夠指定變量的做用域(選擇一個對象),在它的塊做用域內,變量就至關於這個對象的屬性。
var obj={ a: 1, b: 2, c:3 }; // 單調乏味的重複 "obj" obj.a = 2; obj.b = 3; obj.c = 4; // 簡單的快捷方式 with (obj) { a=3; b=4; c=5; }
不被推薦,由於它會影響性能,且不易閱讀(代碼塊內的代碼特別多的狀況,根本不知道這個是普通的變量仍是某個對象的屬性,仍是某個對象的屬性的屬性的屬性)。
try { undefined(); // 執行一個非法操做來強制製造一個異常 } catch (err) { console.log( err ); // 可以正常執行! } console.log( err ); // ReferenceError: err not found
作錯誤狀態傳參的err變量是當前塊的局部變量。
可是若是在catch(err){…}內部var其它變量,並無效果,見下面代碼。
try { var abc='測試try塊中的變量' } catch (err) { var b=2; // 沒有錯誤,不會被執行到的。 } console.log( abc ); // 測試try塊中的變量
try { throw '55'; // 製造一個異常 } catch (err) { var abc='測試catch塊中的變量'; } console.log(abc); // 測試catch塊中的變量
這是隻屬於err參數用的僞塊做用域。
ES6新特性,大神器。在{}
中造成塊做用域,且不會遇到提高
的問題出現。
爲變量顯式聲明塊做用域,有助於回收內存垃圾
。
function process(data) { // 在這裏作點有趣的事情 } // 在這個塊中定義的內容能夠銷燬了! (這裏指的是下面let定義的`someReallyBigData`) { let someReallyBigData = { .. }; process( someReallyBigData ); } var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt){ console.log("button clicked"); }, /*capturingPhase=*/false );
let有一個頗有意思的地方,就是在for循環中。
for (let i=0; i<10; i++) { console.log( i ); } console.log( i ); // ReferenceErrorfor 循環頭部的 let 不只將 i 綁定到了 for 循環的塊中,事實上它將其從新綁定到了循環 的每個迭代中,確保使用上一個循環迭代結束時的值從新進行賦值。
下面經過另外一種方式來講明每次迭代時進行從新綁定的行爲:{ let j; for (j=0; j<10; j++) { let i = j; // 每一個迭代從新綁定! console.log( i ); } }
編譯器在解析做用域時,會對做用域中var聲明的變量、函數進行提高
。
a=2; var a; console.log( a ); // 2
至關於
var a; a=2; console.log( a ); // 2
console.log( a ); // undefined var a=2;
至關於
var a; console.log( a ); // undefined a=2;
函數聲明和變量聲明都會被提高。可是一個值得注意的細節(這個細節能夠出如今有多個「重複」聲明的代碼中)是函數會首先被提高,而後纔是變量。
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
至關於
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包。
function foo() { var a=2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // 媽媽快看呀,這就是閉包! }