摘要:依然持有對該作用域的引用,而這個引用就叫作閉包。循環和閉包正常情況下,我們對這段代碼行為的預期是分別輸出數字,每秒一次,每次一個。
一、作用域
作用域共有兩種主要的工作模型:第一種是最為普遍的,被大多數編程語言所采用的詞法作用域,另外一種叫作動態作用域;
JavaScript所采用的作用域模式是詞法作用域。
1.詞法作用域詞法作用域意味著作用域是由書寫代碼時函數聲明的位置來決定的。編譯的詞法分析階段基本能夠知道全部標識符在哪里以及是如何聲明的,從而能夠預測在執行過程中如何對它們進行查找。
JavaScript 中有兩個機制可以“欺騙”詞法作用域:
eval(..):可以對一段包含一個或多個聲明的“代碼”字符串進行演算,并借此來修改已經存在的詞法作用域(在運行時) ;
with:通過將一個對象的引用當作作用域來處理,將對象的屬性當作作用域中的標識符來處理,從而創建了一個新的詞法作用域(同樣是在運行時) 。
這兩個機制的副作用是引擎無法在編譯時對作用域查找進行優化,因為引擎只能謹慎地認為這樣的優化是無效的。使用這其中任何一個機制都將導致代碼運行變慢。
2.函數作用域和塊級作用域函數作用域: 函數是 JavaScript 中最常見的作用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的作用域中“隱藏”起來,即函數內定于的函數和變量為該函數私有;
塊級作用域:
塊作用域指的是變量和函數不僅可以屬于所處的作用域,也可以屬于某個代碼塊(通常指 { .. } 內部)
ES6前在JavaScript中并不存在塊級作用域( 例外:try/catch 結構在 catch 分句中具有塊作用域);
在 ES6 中引入了 let 關鍵字( var 關鍵字的表親) ,用來在任意代碼塊中聲明變量。 if(..) { let a = 2; } 會聲明一個劫持了 if 的 { .. } 塊的變量,并且將變量添加到這個塊中(另外常量定義const也具有塊級作用域)。
3.函數和變量的提升(1)、提升
函數作用域和塊作用域的行為是一樣的,即,某個作用域內的變量,都將附屬于這個作用域。
引擎會在解釋 JavaScript 代碼之前首先對其進行編譯。編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域將它們關聯起來;
因此包括變量和函數在內的所有聲明都會在任何代碼被執行前首先被處理;
當看到 var a = 2; 時,可能會認為這是一個聲明。但 JavaScript 實際上會將其看成兩個聲明: var a; 和 a = 2; 。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執行階段。
這個過程就好像變量和函數聲明從它們在代碼中出現的位置被“移動”到了最上面。這個過程就叫作提升。
每個作用域都會進行提升操作;
(2)、函數優先
函數聲明和變量聲明都會被提升。但是函數會首先被提升,然后才是變量。
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
會輸出 1 而不是 2 !這個代碼片段會被引擎理解為如下形式:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
var foo 盡管出現在 function foo()... 的聲明之前,但它是重復的聲明(因此被忽略了) ,因為函數聲明會被提升到普通變量之前。
盡管重復的 var 聲明會被忽略掉,但出現在后面的函數聲明還是可以覆蓋前面的。
二、作用域閉包 (1)、理解閉包當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。
在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。
在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
(2)、閉包的用途可以讀取函數內部的變量;
讓變量的值始終保持在內存中。
(3)、閉包的產生實例可以讀取函數內部的變量:
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2 —— 這就是閉包的效果。
在 foo() 執行后,通常會期待 foo() 的整個內部作用域都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的內存空間;
閉包的“神奇”之處正是可以阻止這件事情的發生。事實上內部作用域依然存在,因此沒有被回收,因為 bar() 本身在使用;
拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時間進行引用。
bar() 依然持有對該作用域的引用,而這個引用就叫作閉包。
循環和閉包:
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
正常情況下,我們對這段代碼行為的預期是分別輸出數字 1~5,每秒一次,每次一個。但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次 6:
延遲函數的回調會在循環結束時才執行。事實上,當定時器運行時即使每個迭代中執行的是 setTimeout(.., 0) ,所有的回調函數依然是在循環結束后才會被執行,因此會每次輸出一個 6 出來。
實際情況是盡管循環中的五個函數是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上只有一個 i,即所有函數共享一個 i 的引用 。
解決方案:使用 IIFE在每次迭代中將本次迭代的i傳入創建的作用域并封閉起來;
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); }
在迭代內使用 IIFE 會為每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代中都會含有一個具有正確值的變量供我們訪問。
(4)、使用閉包的注意點
由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。
解決方案:在退出函數之前,將不使用的局部變量全部刪除。
閉包會在父函數外部,改變父函數內部變量的值。所以,如果把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/52382.html
摘要:依然持有對該作用域的引用,而這個引用就叫作閉包。循環和閉包正常情況下,我們對這段代碼行為的預期是分別輸出數字,每秒一次,每次一個。 一、作用域 作用域共有兩種主要的工作模型:第一種是最為普遍的,被大多數編程語言所采用的詞法作用域,另外一種叫作動態作用域; JavaScript所采用的作用域模式是詞法作用域。 1.詞法作用域 詞法作用域意味著作用域是由書寫代碼時函數聲明的位置來決定...
摘要:閉包面試題解由于作用域鏈機制的影響,閉包只能取得內部函數的最后一個值,這引起的一個副作用就是如果內部函數在一個循環中,那么變量的值始終為最后一個值。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第8天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了...
摘要:查詢是在作用域鏈中,一級級的往上查找該變量的引用。作用域和作用域鏈作用域的概念,應該兩張圖幾句話就能解釋吧。這個建筑代表程序中的嵌套作用域鏈。一層嵌一層的作用域形成了作用域鏈,變量在作用域鏈中的函數內得到了自己的定義。 javascript作用域和閉包之我見 看了《你不知道的JavaScript(上卷)》的第一部分——作用域和閉包,感受頗深,遂寫一篇讀書筆記加深印象。路過的大牛歡迎指點...
摘要:作用域和閉包是最重要的概念之一,想要進一步學習,就必須理解作用域和閉包的工作原理。全局和局部作用域的關系在函數體內,局部變量的優先級高于同名的全局變量。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。 作用域和閉包是 JavaScript 最重要的概念之一,想要進一步學習 JavaScript,就必須理解 JavaScript 作用域和閉包的工作原理。 作用域 任何...
閱讀 3400·2021-11-24 10:30
閱讀 3269·2021-11-22 15:29
閱讀 3706·2021-10-28 09:32
閱讀 1254·2021-09-07 10:22
閱讀 3336·2019-08-30 15:55
閱讀 3619·2019-08-30 15:54
閱讀 3494·2019-08-30 15:54
閱讀 2833·2019-08-30 15:44