摘要:依然持有對該作用域的引用,而這個引用就叫作閉包。無論通過何種手段將內部函數傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執行這個函數都會使用閉包。
因為最近項目比較少,閑來覺得需要學習《你不知道的JavaScript》;跟大家分享一下;
什么是作用域需要一套設計良好的規則來存儲變量,并且之后可以方便地找到這些變量。這套規
則被稱為作用域
1.var a: 編譯器會詢問作用域是否存在變量a;如果是,編譯器會忽略該聲明,繼續進行編譯。否則它會要求作用域在當前作用域的集合中聲明一個新的變量,并命名為a;接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值操作。
2.引擎運行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作a的變量。如果否,引擎就會
使用這個變量;如果不是,引擎會繼續查找該變量如果引擎最終找到了a變量,就會將2賦值給它。否則引擎就會舉手示意并拋出一個異常!
RHS查詢:簡單地查找某個變量的值
LHS查詢:試圖找到變量的容器本身,從而可以對其賦值
作用域嵌套在概念上最好將其理解為“賦值操作的目標是誰(LHS)”以及“誰是賦值操作的源頭(RHS)”。
LHS:對哪個 賦值 就對哪個進行LHS引用,可以理解為賦值操作的目標。
RHS:需要 獲取 哪個變量的值,就對哪個變量的值進行RHS引用,理解為賦值操作的源頭。
當一個塊或函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套。因此,在當前作用域中無法
找到某個變量時,引擎就會在外層嵌套的作用域中繼續查找,直到找到該變量,或抵達最外層的作用域(也就是全局作用域)為止。
遍歷嵌套作用域鏈的規則很簡單:引擎從當前的執行作用域開始查找變量,如果找不到,就向上一
級繼續查找。當抵達最外層的全局作用域時,無論找到還是沒找到,查找過程都會停止。
如果RHS查詢在所有嵌套的作用域中遍尋不到所需的變量,引擎就會拋出ReferenceError異常
在嚴格模式中LHS查詢失敗時,并不會創建并返回一個全局變量,引擎會拋出同RHS查詢失敗時類似的ReferenceError異常。
如果RHS查詢找到了一個變量,但是你嘗試對這個變量的值進行不合理的賦值,那么引擎會
拋出另外一種類型的異常,叫作TypeError。
ReferenceError同作用域判別失敗相關,而TypeError則代表作用域判別成功了,但是對結果的操作
是非法或不合理的。
遮蔽效應在多層的嵌套作用域中可以定義同名的標識符,這叫作“遮蔽效應”(內部的標識符“遮蔽”了外部的標識符)。作用域查找始終從運行時所處的最內部作用域開始,逐級向外或者說向上進行,直到遇見第一個匹配的標識符為止。
全局變量會自動成為全局對象(比如瀏覽器中的window對象)的屬性,所以如果要逃避遮蔽效應
可以通過 window對象
window.a //得到的是全局定義的a變量;全局命名空間
庫通常會在全局作用域中聲明一個名字足夠獨特的變量,通常是一個對象。這個對象被用作
庫的命名空間,所有需要暴露給外界的功能都會成為這個對象(命名空間)的屬性,而不是將自己
的標識符暴漏在頂級的詞法作用域中。
我們已經知道,在任意代碼片段外部添加包裝函數,可以將內部的變量和函數定義“隱藏”起來,外
部作用域無法訪問包裝函數內部的任何內容。
雖然這種技術可以解決一些問題,但是它并不理想,因為會導致一些額外的問題。首先,必須聲明
一個具名函數foo(),意味著foo這個名稱本身“污染”了所在作用域(在這個例子中是全局作用域)。
其次,必須顯式地通過函數名(foo())調用這個函數才能運行其中的代碼。
更加理想的方式
var a = 2; (function foo(){ // <-- 添加這一行 var a = 3; console.log( a ); // 3 })(); // <-- 以及這一行 console.log( a ); // 2
以(function...而不僅是以function...開始。函數會被當作函數表達式而不是一個標準的函數聲明 來處理。
函數聲明和函數表達式之間最重要的區別是它們的名稱標識符將會綁定在何處。foo被綁定在函數表達式自身的函數中而不是所在作用域中。
換句話說,(function foo(){ .. })作為函數表達式意味著foo只能在..所代表的位置中被訪問,外
部作用域則不行。foo變量名被隱藏在自身中意味著不會非必要地污染外部作用域。
很多人都更喜歡另一個改進的形式:(function(){ .. }())。這兩種形式在功能上是一致的。選擇哪個全憑個人喜好.
塊 作用域for (var i=0; i<10; i++) { console.log( i ); }
我們在for循環的頭部直接定義了變量i,通常是因為只想在for循環內部的上下文中使用i,而忽
略了i會被綁定在外部作用域(函數或全局)中的事實。
JavaScript的ES3規范中規定try/catch的catch分句會創建一個塊作用域,其中聲明的變量僅在catch內部有效。
try { undefined(); // 執行一個非法操作來強制制造一個異常 }c atch (err) { console.log( err ); // 能夠正常執行! } c onsole.log( err ); // ReferenceError: err not found
ES6改變了現狀,引入了新的let關鍵字,提供了除var以外的另一種變量聲明方式
let關鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. }內部)。只要聲明是有效的,在聲明中的任意位置都可以使用{ .. }括號來為let創建一個用于綁定的塊。
為變量顯式聲明塊作用域,并對變量進行本地綁定是非常有用的工具,可以讓引擎清楚地知道沒有必要繼續保存那些變量(當塊的變量沒有被引用時就銷毀);
const除了let以外,ES6還引入了const,同樣可以用來創建塊作用域變量,但其值是固定的(常量)。之后
任何試圖修改值的操作都會引起錯誤。
解析兩個輸出
a = 2; var a; console.log( a );//2
console.log( a ); //undefined var a = 2;
編譯器順序
當你看到var a = 2;時,可能會認為這是一個聲明。但JavaScript實際上會將其看成兩個聲 明:var a;和a =
2;。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執 行階段。
我們的第二個代碼片段實際是按照以下流程處理的:
var a; console.log( a ); a = 2;
這個過程就好像變量和函數聲明從它們在代碼中出現的位置被“移動”到了最上面。這個過程就叫作 提升。
換句話說,先有蛋(聲明)后有雞(賦值)。
函數聲明會優先于變量聲明
foo(); // 1 var foo; function foo() { console.log( 1 ); } f oo = function() { console.log( 2 ); };
相當于
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };閉包
當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之
外執行。
詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫代碼時將 變量和塊作用域寫在哪里來決定的
function foo() { var a = 2; function bar() { console.log( a );} r eturn bar; } var baz = foo(); baz(); // 2 ———— 朋友, 這就是閉包的效果。
我們將bar()函數本身當作一個值類型進行傳遞。
函數bar()的詞法作用域能夠訪問foo()的內部作用域。
在foo()執行后,其返回值(也就是內部的bar()函數)賦值給變量baz并調用baz(),實際上只是通過不同的標識符引用調用了內部的函數bar()。
因為我們知道引擎有垃圾回收器用來釋放不再使用的內存空間。由于看上去foo()的內容不會再被使用,所以很自然地會考慮對其進行回收。在foo()執行后,通常會期待foo()的整個內部作用域都被銷毀。
而閉包的“神奇”之處正是可以阻止這件事情的發生。事實上內部作用域依然存在,因此沒有被回收。
誰在使用這個內部作用域?原來是bar()本身在使用。拜bar()所聲明的位置所賜,它擁有涵蓋foo()內部作用域的閉包,使得該作用域能夠一直存活,以供bar()在之后任何時間進行引用。
一個神奇的例子bar()依然持有對該作用域的引用,而這個引用就叫作閉包。
無論通過何種手段將內部函數傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引
用,無論在何處執行這個函數都會使用閉包。本質上無論何時何地,如果將函數(訪問它們各自的詞法作用域)當作第一級的值類型并到處傳遞,你就會看到閉包在這些函數中的應用。只要使用了回調函數,實際上就是在使用閉包!
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } //每秒出現一個6
解析:延遲函數的回調會在循環結束時才執行。事實上,當定時器運行時即使每個迭代中執行的是setTimeout(..,
0),所有的回調函數依然是在循環結束后才會被執行,因此會每次輸出一個6出來。我們試圖假設循環中的每個迭代在運行時都會給自己“捕獲”一個i的副本。但是根據作用域的工作原理,實際情況是盡管循環中的五個函數是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上只有一個i。
改進寫法
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); } //每秒從1到6依次輸出
解析:在迭代內使用IIFE(自執行函數)會為每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代中都會含有一個具有正確值的變量供我們訪問。
細說:
for (var i = 0; i <= 5; i++) { console.log(i) }
我們都知道上面的代碼可以輸出1到6;因此可以明白
for (var i=1; i<=5; i++) { (function(j) { })( i ); } //這一層,是會不斷的獲取到正確的i值;
然后,由于延遲函數的回調 使用了閉包;每次閉包都會保存IIFE 的有正確值的作用域;
閉包的應用-- 模塊直接抄代碼看:
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
除了返回一個對象,還可以返回一個函數很好理解,coolmodule()返回一個對象,這個對象通過不同的標識符引用調用了內部的函數;這些函數就是coolmodule的閉包,具有訪問coolmodule作用域的能力;
閉包的應用-- 實現模塊從模塊中返回一個實際的對象并不是必須的,也可以直接返回一個內部函數。jQuery就是
一個很好的例子。jQuery和$標識符就是jQuery模塊的公共API,但它們本身都是函數(由于函數也是對象,它們本身也可以擁有屬性)。因此,jq的方法有兩種,通過$.xxx() 運行的是jq的屬性方法;通過$() 運行的是jq的函數方法;
模塊模式需要具備兩個必要條件。
必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會創建一個新的模塊實例)。
封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態。
單例模式
var foo = (function CoolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } f unction doAnother() { console.log( another.join( " ! " ) ); }return { doSomething: doSomething, doAnother: doAnother }; })(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89648.html
摘要:的變量作用域是基于其特有的作用域鏈的。需要注意的是,用創建的函數,其作用域指向全局作用域。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。 作用域 定義 在編程語言中,作用域控制著變量與參數的可見性及生命周期,它能減少名稱沖突,而且提供了自動內存管理 --javascript 語言精粹 我理解的是,一個變量、函數或者成員可以在代碼中訪問到的范圍。 js的變量作...
摘要:作用域分為詞法作用域和動態作用域。這樣就形成了一個鏈式的作用域。一般情況下,當函數執行完畢時,里面的變量會被自動銷毀。而能夠訪問到這個在的編譯階段就已經定型了詞法作用域。 什么是作用域?在當前運行環境下,可以訪問的變量或函數的范圍。作用域分為詞法作用域和動態作用域。詞法作用域是在js代碼編譯階段就確定下來的; 對應的,with和eval語句會產生動態作用域。 會產生新的作用域的情況: ...
摘要:是詞法作用域工作模式。使用可以將變量綁定在所在的任意作用域中通常是內部,也就是說為其聲明的變量隱式的劫持了所在的塊級作用域。 作用域與閉包 如何用js創建10個button標簽,點擊每個按鈕時打印按鈕對應的序號? 看到上述問題,如果你能看出來這個問題實質上是考對作用域的理解,那么恭喜你,這篇文章你可以不用看了,說明你對作用域已經理解的很透徹了,但是如果你看不出來這是一道考作用域的題目,...
摘要:是詞法作用域工作模式。使用可以將變量綁定在所在的任意作用域中通常是內部,也就是說為其聲明的變量隱式的劫持了所在的塊級作用域。 作用域與閉包 如何用js創建10個button標簽,點擊每個按鈕時打印按鈕對應的序號? 看到上述問題,如果你能看出來這個問題實質上是考對作用域的理解,那么恭喜你,這篇文章你可以不用看了,說明你對作用域已經理解的很透徹了,但是如果你看不出來這是一道考作用域的題目,...
摘要:是詞法作用域工作模式。使用可以將變量綁定在所在的任意作用域中通常是內部,也就是說為其聲明的變量隱式的劫持了所在的塊級作用域。 作用域與閉包 如何用js創建10個button標簽,點擊每個按鈕時打印按鈕對應的序號? 看到上述問題,如果你能看出來這個問題實質上是考對作用域的理解,那么恭喜你,這篇文章你可以不用看了,說明你對作用域已經理解的很透徹了,但是如果你看不出來這是一道考作用域的題目,...
閱讀 2675·2023-04-25 18:10
閱讀 1611·2019-08-30 15:53
閱讀 2803·2019-08-30 13:10
閱讀 3224·2019-08-29 18:40
閱讀 1133·2019-08-23 18:31
閱讀 1205·2019-08-23 16:49
閱讀 3407·2019-08-23 16:07
閱讀 881·2019-08-23 15:27