摘要:的分句會創建一個塊作用域,其聲明的變量僅在中有效。而閉包的神奇作用是阻止此事發生。依然持有對該作用域的引用,而這個引用就叫做閉包。當然,無論使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時都可以觀察到閉包。
LHS:賦值操作的目標是誰?
比如:
a = 2;
RHS:誰是賦值操作的源頭?
比如:
console.log(2);
作用域嵌套:遍歷嵌套作用域鏈的規則:引擎從當前的執行作用域開始查找變量,如果找不到,就向上一級繼續查找。當抵達最外層的全局作用域時,無論是否找到都會停止。
異常:為什么區分LHS和RHS是一件重要的事情?
如果RHS查詢在所有嵌套的作用域中遍尋不到所需的變量,引擎就會拋出ReferenceError異常。
當引擎在執行LHS查詢時,如果在頂層作用域也無法找到目標變量,全局作用域就會創建一個具有該名稱的變量,并將其返回給引擎。(非嚴格模式下)
如果RHS查詢找到了一個變量,但你嘗試對這個變量的值進行不合理的操作,比如試圖對一個非函數類型的值進行函數調用,或者引用null或undefined類型的值中的屬性,引擎會拋出TypeError.
ReferenceError同作用域判別失敗相關,TypeError則代表作用域判別成功但對結果的操作是非法或不合理的。
詞法作用域
詞法作用域就是定義在詞法階段的作用域。詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪里來決定的,因此當詞法分析器處理代碼時會保持作用域不變。
作用域查找會在找到第一個匹配的標識符時停止:遮蔽效應。(全局變量可以使用window.a來訪問)
欺騙詞法
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和譯者趙望野,感謝自由和開源世界
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/88127.html
摘要:比如程序會被分解為解析語法分析將詞法單元流轉換成一個由元素逐級嵌套所組成的代表了程序語法接口的書,又稱抽象語法樹。代碼生成將抽象語法樹轉換為機器能夠識別的指令。 showImg(https://segmentfault.com/img/remote/1460000009682106?w=640&h=280); 本文首發在我的個人博客:http://muyunyun.cn/ 《你不知道的...
摘要:建筑的頂層代表全局作用域。實際的塊級作用域遠不止如此塊級作用域函數作用域早期盛行的立即執行函數就是為了形成塊級作用域,不污染全局。這便是閉包的特點吧經典面試題下面的代碼輸出內容答案個如何處理能夠輸出閉包方式方式下一篇你不知道的筆記 下一篇:《你不知道的javascript》筆記_this 寫在前面 這一系列的筆記是在《javascript高級程序設計》讀書筆記系列的升華版本,旨在將零碎...
摘要:而閉包的神奇之處正是可以阻止事情的發生。拜所聲明的位置所賜,它擁有涵蓋內部作用域的閉包,使得該作用域能夠一直存活,以供在之后任何時間進行引用。依然持有對該作用域的引用,而這個引用就叫閉包。 引子 先看一個問題,下面兩個代碼片段會輸出什么? // Snippet 1 a = 2; var a; console.log(a); // Snippet 2 console.log(a); v...
摘要:最近剛剛看完了你不知道的上卷,對有了更進一步的了解。你不知道的上卷由兩部分組成,第一部分是作用域和閉包,第二部分是和對象原型。附錄詞法這一章并沒有說明機制,只是介紹了中的箭頭函數引入的行為詞法。第章混合對象類類理論類的機制類的繼承混入。 最近剛剛看完了《你不知道的 JavaScript》上卷,對 JavaScript 有了更進一步的了解。 《你不知道的 JavaScript》上卷由兩部...
摘要:一作用域域表示的就是范圍,即作用域,就是一個名字在什么地方可以使用,什么時候不能使用。概括的說作用域就是一套設計良好的規則來存儲變量,并且之后可以方便地找到這些變量。 一、作用域 域表示的就是范圍,即作用域,就是一個名字在什么地方可以使用,什么時候不能使用。想了解更多關于作用域的問題推薦閱讀《你不知道的JavaScript上卷》第一章(或第一部分),從編譯原理的角度說明什么是作用域。概...
閱讀 1430·2021-10-08 10:05
閱讀 3079·2021-09-26 10:10
閱讀 895·2019-08-30 15:55
閱讀 516·2019-08-26 11:51
閱讀 451·2019-08-23 18:10
閱讀 3871·2019-08-23 15:39
閱讀 672·2019-08-23 14:50
閱讀 779·2019-08-23 14:46