LHS:賦值操做的目標是誰?
好比:閉包
a = 2;
RHS:誰是賦值操做的源頭?
好比:函數
console.log(2);
做用域嵌套:遍歷嵌套做用域鏈的規則:引擎從當前的執行做用域開始查找變量,若是找不到,就向上一級繼續查找。當抵達最外層的全局做用域時,不管是否找到都會中止。
異常:爲何區分LHS和RHS是一件重要的事情?
若是RHS查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出ReferenceError異常。
當引擎在執行LHS查詢時,若是在頂層做用域也沒法找到目標變量,全局做用域就會建立一個具備該名稱的變量,並將其返回給引擎。(非嚴格模式下)
若是RHS查詢找到了一個變量,但你嘗試對這個變量的值進行不合理的操做,好比試圖對一個非函數類型的值進行函數調用,或者引用null或undefined類型的值中的屬性,引擎會拋出TypeError.
ReferenceError同做用域判別失敗相關,TypeError則表明做用域判別成功但對結果的操做是非法或不合理的。code
詞法做用域對象
詞法做用域就是定義在詞法階段的做用域。詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變。
做用域查找會在找到第一個匹配的標識符時中止:遮蔽效應。(全局變量可使用window.a來訪問)ip
欺騙詞法內存
eval():能夠對一段包含一個或多個聲明的代碼字符串進行演算,並藉此來修改已經存在的詞法做用域(在運行時)作用域
function foo(str, a){ eval( str ); console.log(a,b); } var b = 2 foo("var b = 3;",1); //1,3
with關鍵字:本質上是用過講一個對象的引用看成做用域來處理,將對象的屬性看成做用域中的標識符來處理,從而建立了一個新的詞法做用域。開發
function foo(obj) { with (obj) { a = 2; } } var o1 = { a:3 }; var o2 = { b:3 }; foo(o1); console.log( o1.a ); //2 foo(o2); console.log( o2.a ); // undefined console.log(a); //2---很差,a被泄露到全局做用域上了。
函數做用域的含義指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用。
規避衝突:字符串
function foo() { function bar(a) { i = 3; //不當心懂了for循環所屬做用域中的i console.log( a + i ); } for (var i=0; i<10; i++) { bar( i*2 ); //進入死循環。 } } foo();
全局命名空間:當程序加載了多個第三方庫時,若是他們沒有妥善的將內部私有的函數或變量隱藏起來,就很容易產生衝突。回調函數
模塊管理
爲了避免污染做用域,可使用包裝函數來解決這個問題。包裝函數的聲明以(function.. 開始。包裝函數會自動運行,是一個表達式。
IIFE:當即執行函數表達式(Immediately Invoked Function Expression)
var a = 2; (function foo(){ var a = 3; console.log(a); //3 })(); //防止了foo這個名稱污染了做用域 console.log(a); //2
匿名函數表達式的利弊
setTimeout( function() { console.log("+1s,WTF!") },100);
行內函數表達式
setTimeout( function haveName() { console.log("+1s,WTF!") },100);
塊做用域:幾乎形同虛設,只能靠開發者自覺了。在塊做用域內聲明的變量都會屬於外部做用域。表面上看如此,但若是深刻探究。
用with從對象中建立出的做用域僅在with聲明中而非外部做用域中有效。
try/catch的catch分句會建立一個塊做用域,其聲明的變量僅在catch中有效。
let關鍵字能夠將變量綁定到所在的任意做用域中。let聲明附屬於一個新的做用域而不是當前的函數做用域(也不屬於全局做用域)。
var foo = true; if (foo) { let bar = foo * 2; bar = something(bar); console.log(bar); } console.log(bar); //ReferenceError
先有雞仍是先有蛋的問題:
Demo1:
a = 2; var a; console.log(a); //2
Demo2:
console.log(a); //undefined var a = 2;
事實是先有蛋(聲明)後有雞(賦值)。實際處理以下:
demo1實際:
var a; a = 2; console.log(a);
demo2實際:
var a; console.log(a); a = 2;
只有聲明自己會被提高,而賦值或者其餘運行邏輯會留在本地。
foo(); //TypeError var foo = function bar() { // ... };
demo3:
foo(); // TypeError bar(); //ReferenceError var foo = function bar(){ // ... }
上述代碼提高後實際理解形式:
var foo; foo(); bar(); foo = function() { var bar = ..self.. //... }
提高過程函數優先,而後纔是變量:
foo(); //1 var foo; function foo() { console.log(1); } foo = function() { console.log(2); }
上述代碼會被理解成如下形式:
function foo() { console.log(1); } foo(); foo = function() { console.log(2); };
儘管var foo出如今function foo()以前,但它是重複的聲明,所以被忽略。由於函數聲明會被提高到普通變量以前。
聲明自己會被提高,但包括函數表達式的賦值在內的賦值操做並不會提高。
當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外進行。
function foo() { var a = 2; function bar() { console.log(a); } return bar; } var baz = foo(); baz(); //2 這就是閉包的效果
函數bar()詞法做用域可以訪問foo()的內部做用域。而後咱們將bar()函數自己看成一個值類型進行傳遞。咱們將bar所引用的函數對象自己看成返回值。
在foo()執行後,其返回值賦值給變量baz並調用baz(),其實是經過不一樣的標識符引用調用了內部的函數bar()。
bar()顯然能夠被正常執行。但在這個例子中,它在本身定義的詞法做用域之外的地方執行。
在foo()執行後,一般會期待foo()的整個內部做用域都被銷燬,由於引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲foo()的內容不會再被使用,因此會被回收。
而閉包的神奇做用是阻止此事發生。事實上內部做用域依舊存在,由於bar()自己在使用。
拜bar()所聲明的位置所賜,它擁有涵蓋foo()內部做用域的閉包,使得該做用域可以一直存活,以供bar()在以後任什麼時候間進行引用。
bar()依然持有對該做用域的引用,而這個引用就叫作閉包。
固然,不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包。
var fn; function foo() { var a = 2; function baz() { console.log(a); } fn = baz; //將baz分配給全局變量 } function bar() { fn(); } foo(); bar(); //2
不管經過何種手段將內部函數傳遞到所在的詞法做用域外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。
本質上不管什麼時候何地。若是將函數(訪問它們各自的詞法做用域)看成第一級的值類型並處處傳遞,你就會看到閉包在這類函數中的應用。(好比使用了回調函數)
for (var i=1; i<=5; i++) { setTimeout(function timer() { console.log(i); }, i*1000); }
咱們預期上述代碼依次輸出1,2,3,4,5。實際會輸出五次6。由於輸出顯示的是循環結束時i的值。
由於延遲函數的回調會在循環結束後才執行。根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i.
修改以下:
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log(j); }, j*1000); })(i); }
再迭代中使用IIFE會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。
將塊做用域和閉包聯手後:
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000); }
模塊也是利用閉包的一個好方法:
function CoolModule() { var something = 'cool'; var another = [1,2,3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join("!")); } return { doSomething: doSomething, doAnother: doAnother }; } var foo = CoolModule(); foo.doSomething(); //cool foo.doAnother(); //1!2!3
這就是JavaScript中最經常使用的模塊,doSomething()和doAnother()函數具備涵蓋模塊實例內部做用域的閉包。
總結一下,模塊模式須要兩個必要條件:
1.必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會建立一個新的模塊實例)。
2.封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態。
也能夠用單例模式來實現,這種狀況適用於只須要一個實例的情景:
var foo = (function CoolModule() { var something = 'cool'; var another = [1,2,3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join("!")); } return { doSomething: doSomething, doAnother: doAnother }; })(); foo.doSomething(); foo.doAnother();
模塊模式也能夠接受參數,再也不贅述。
最後總結一下閉包:
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這是就產生了閉包。
JavaScript並不具備動態做用域,它只有詞法做用域。
function foo() { console.log(a); } function bar() { var a = 3; foo(); } var a = 2; bar();
實際上上述代碼輸出2,由於詞法做用域讓foo()中的a經過RHS引用到了全局做用域中的a,所以會輸出2.若是JavaScript有動態做用域,那麼會輸出3,可是JavaScript並無動態做用域。
第一部分完 感謝做者Kyle Simpson和譯者趙望野,感謝自由和開源世界