下一篇:《你不知道的javascript》筆記_thisjavascript
這一系列的筆記是在《javascript高級程序設計》讀書筆記系列的昇華版本,旨在將零碎未知的知識總結java
在傳統編譯語言的流程中,程序中的一段源代碼在執行以前會經歷三個步驟,統稱爲編譯
:面試
1. 分詞/詞法分析(Tokenizing/Lexing)編程
這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)
。例如,考慮程序var a = 2;
。這段程序一般會被分解成爲下面這些詞法單元:var、a、=、2 、;
。空格是否會被看成詞法單元,取決於空格在這門語言中是否具備意義
2. 解析/語法分析(Parsing)segmentfault
這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的樹。這個樹被稱爲
抽象語法樹
(Abstract Syntax Tree,AST)
3. 代碼生成數組
將 AST 轉換爲可執行代碼的過程稱被稱爲代碼生成。拋開具體細節,簡單來講就是有某種方法能夠將
var a = 2;
的 AST 轉化爲一組機器指令,用來建立一個叫做 a 的變量(包括分配內存等),並將一個值儲存在 a 中
相對於上面的流程,javascript
在語法分析和代碼生成階段有特定的步驟來對運行性能進行優化,包括對冗餘元素進行優化等。閉包
引擎:從頭至尾負責整個JavaScript
程序的編譯及執行過程編譯器:負責語法分析及代碼生成等髒活累活異步
做用域:負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限編程語言
RHS 查詢:當變量出如今賦值操做的右側時進行 RHS 查詢
LHS 查詢:當變量出如今賦值操做的左側時進行 LHS 查詢(賦值、傳參、函數執行)函數
RHS查詢異常:RHS 查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 異常
LHS查詢異常:非嚴格模式下,LHS 查詢失敗會在全局建立變量;在嚴格模式中 LHS 查詢失敗時,並不會建立並返回一個全局變量,引擎會拋出同 RHS 查詢失敗時相似的 ReferenceError 異常
javascript
引擎執行代碼var a = 2;
的過程?
編譯階段:var a;
,若是做用域內已存在變量 a,則忽略;若不存在,則在該做用域內聲明
執行階段:a = 2;
,對 a 進行 LHS 引用,並對其賦值
負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限
通俗的說,做用域是維護變量並肯定訪問權限的一套規則
詞法做用域就是定義在詞法階段的做用域。換句話說,詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變(大部分狀況下是這樣的)
下面有個簡單的做用域嵌套的例子:
【1】包含着整個全局做用域,其中只有一個標識符: foo。
【2】包含着 foo 所建立的做用域,其中有三個標識符: a、bar 和 b,可訪問全局做用域變量。
【3】包含着 bar 所建立的做用域,其中只有一個標識符: c,可訪問foo和全局做用域變量。
另外有兩個比較特殊的欺騙詞法
機制:
eval(..)
函數with
關鍵字這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎地認爲這樣的優化是無效的。使用這其中任何一個機制都將致使代碼運行變慢。 不要使用它們。
書中對做用域鏈和做用域查找作了一個很是形象的比喻,以下圖
這個建築表明程序中的嵌套做用域鏈。第一層樓表明當前的執行做用域,也就是你所處的位置。建築的頂層表明全局做用域。
LHS 和 RHS 引用都會在當前樓層進行查找,若是沒有找到,就會坐電梯前往上一層樓, 若是仍是沒有找到就繼續向上,以此類推。一旦抵達頂層(全局做用域),可能找到了你所需的變量,也可能沒找到,但不管如何查找過程都將中止
做用域查找會在找到第一個匹配的標識符時中止
早期的javascript語句中塊級做用域就是函數塊,這是在讀本書以前我粗淺的認識。實際的塊級做用域遠不止如此
塊級做用域:
(1)函數做用域
早期盛行的當即執行函數(IIFE)
就是爲了造成塊級做用域,不污染全局。經常使用的寫法有:
(function(形參){函數體})(實參) (function(形參){函數體}(實參)) !function(形參){函數體}(實參)
(2) with
關鍵字
(3) try/catch
語句
Google 維護着一個名爲 Traceur 的項目,該項目正是用來將 ES6 代碼轉換成兼容 ES6 以前 的環境(大部分是 ES5,但不是所有),下面是用來兼容低版本建立塊級做用域的寫法:
{ try { throw undefined; } catch (a) { a = 2; console.log( a ); } }
(4) let/const
關鍵字
在以前的兩篇文章中對變量提高(預解析)有比較充分的說明:
《javascript高級程序設計》筆記:變量對象與預解析
《javascript高級程序設計》筆記:內存與執行環境
本書中定義: 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。MDN定義:閉包是函數和聲明該函數的詞法環境的組合
我的理解:當外部可以訪問到某個函數的私有變量時,就會產生閉包(不嚴謹,僅用於理解)
兩個經典的閉包例子:
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc(); // 'Mozilla'
思考:myFunc
是執行makeFunc
時建立的displayName
函數實例的引用,爲何執行myFunc
時會打印出makeFunc
中私有變量name
呢?
解釋:閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的全部局部變量
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
分析:按照閉包能暫存變量的思路,執行makeAdder
時,會把參數暫存在所return的函數中,當再次執行函數時,會把兩次的參數之和輸出
閉包在js編程中隨處可見,書中有這樣一個結論:
在定時器、事件監聽器、 Ajax 請求、跨窗口通訊、Web Workers 或者任何其餘的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包!
定時器閉包案例:
function wait(message) { setTimeout( function timer() { console.log( message ); }, 1000 ); } wait( "Hello, closure!" );
事件監聽閉包案例:
function setupBot(name, selector) { $(selector).click( function activator() { console.log( "Activating: " + name ); }); } setupBot( "Closure Bot 1", "#bot_1" ); setupBot( "Closure Bot 2", "#bot_2" );
上面的案例中,有個相同的特色:先定義函數,後執行函數時可以調用到函數中的私有變量或者實參。這即是閉包的特色吧
(1)下面的代碼輸出內容?
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
答案:5個6
(2)如何處理可以輸出1~5
// 閉包方式 for (var i=1; i<=5; i++) { (function(index) { setTimeout( function timer() { console.log( index ); }, index*1000 ); })(i) } // ES6 方式 for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }