若有問題,歡迎指教。更多內容請關注 GitHub
做用域是追蹤全部變量的方式,是代碼的當前上下文以及對變量的訪問權限。瞭解做用域,能夠知道變量/函數在何處可訪問。javascript
JavaScript
使用詞法做用域,這種方法容許做用域嵌套,所以外部做用域包含內部做用域。css
若是一個變量在全部函數或花括號({})以外聲明,則它是在全局做用域內定義的前端
全局變量能夠在代碼的任何地方使用。java
const name = 'sueRimn'; function person () { console.log(name); } console.log(name); // 'sueRimn' person() // 'sueRimn'
雖然能夠在全局範圍內聲明變量,但不建議這樣作,由於存在命名衝突的可能性。git
若是使用const
或let
聲明變量,那麼每當發生名稱衝突時,都會拋錯。這是不可取的。github
let name = 'sueRimn'; let name = '八至'; // 報錯
若是使用var
聲明變量,第二個變量會在聲明後覆蓋第一個變量。這也不可取,由於代碼將很難調試。web
var name = 'sueRimn'; var name = '八至'; console.log(name); // '八至'
因此,你應該聲明局部變量,而不是全局變量。瀏覽器
這隻適用於web瀏覽器中的JavaScript。
只在代碼的特定部分中可用的變量被認爲是在局部做用域中。這些變量也稱爲局部變量。閉包
在JavaScript
中,有兩種局部做用域:函數做用域和塊做用域。函數
當在一個大括號({})內聲明一個const
或let
變量時,只能在那個大括號內訪問這個變量。
{ let name = 'sueRimn'; console.log(name); // 'sueRimn' } console.log(name); // error, name is not defined
塊做用域是函數做用域的一個子集,由於函數須要用花括號聲明(除非使用帶隱式返回的箭頭函數)。
在函數中聲明變量時,只能在函數中訪問該變量,對變量的訪問僅限於函數的局部做用域。
function person () { let name = 'sueRimn'; console.log(name); } person(); // 'sueRimn' console.log(name); // 報錯 name is not defined
當使用函數聲明聲明函數時,老是將其提高到當前範圍的頂部,如下兩種結果是同樣的:
person(); // 'sueRimn is beautiful' function person () { console.log('sueRimn is beautiful'); } person(); // 'sueRimn is beautiful'
當使用函數表達式代表時,函數不會提高到當前範圍的頂部。
person(); // 報錯 person is not defined const person = () =>{ console.log('sueRimn is beautiful'); } person(); // 'sueRimn is beautiful'
因此,儘可能在使用函數以前聲明它。
若是分別獨立聲明函數,即便函數之間能夠彼此調用,可是沒法訪問彼此的變量,由於每一個函數的做用域是獨立的。
function name () { const name = 'sueRimn'; } function age () { const age = '22' name() console.log(name); // error name id not defined. }
當在一個函數中定義另外一個函數時,內部函數能夠訪問外部函數的做用域。函數嵌套也會致使做用域嵌套,做用域嵌套也稱爲詞法做用域或閉包,也成爲靜態做用域。
可是,外部函數沒法訪問內部函數的做用域。就像單向玻璃,你在裏面能夠看見外面,外面的看不見裏面。
function person () { let name = 'sueRimn'; function my () { console.log('my name is' + name); } console.log(name); my(); } // 打印結果是: 'sueRimn' 'my name is sueRimn'
JavaScript屬於解釋型語言,JavaScript的執行分爲解釋和執行兩個階段:
解釋階段:
執行階段:
靜態做用域是指函數定義決定了函數的做用域。JavaScript採用的是靜態做用域。JavaScript解釋階段便會肯定做用域規則,所以做用域在函數定義時就已經肯定了,而不是在函數調用時肯定。
執行上下文是函數執行以前建立的,即在函數執行準備階段建立好的。
執行上下文最明顯的就是this的指向是執行時肯定的,即函數調用決定執行上下文的指向。
由於 JavaScript
採用的是詞法做用域(靜態做用域),函數定義時肯定本身的做用域做爲該函數的屬性,做用域沒法改變,一直保存至函數銷燬。
因此說函數定義時是基於靜態做用域的,由於即便函數不調用,其[[scope]]屬性也會一直存在,而且保持不變。
每一個上下文都有本身的變量對象,對於全局上下文,它是全局對象自身;對於函數,它是活動對象。
當查找變量對象時,計算機會從當前上下文的變量對象中找,若是找不到,就會從父級上下文也就是層層往上查找,直到全局上下文,到那時還找不到,就會拋出ReferenceError
。
做用域鏈正是內部上下文全部變量對象的鏈表,用於變量查詢。
函數上下文的做用域鏈在函數調用時建立的,包含活動對象和這個函數內部的[[scope]]
屬性。
由於當函數調用時,會生成執行上下文,此執行上下文的[[scope]]
和定義函數時的[[scope]]
是不一樣的,執行上下文的[[scope]]
是在函數定義時的[[scope]]
屬性基礎上又新增一個當前AO對象構成的。
所以,函數定義時候的[[scope]]
做爲函數的屬性,函數執行時候的[[scope]]
做爲函數執行上下文的屬性。
通常狀況下,一個做用域鏈包括父級變量對象(variable object)(做用域鏈的頂部)、函數自身變量VO和活動對象(activation object)。
當查找標識符的時候,會從做用域鏈的活動對象部分開始查找,而後(若是標識符沒有在活動對象中找到)查找做用域鏈的頂部,循環往復,就像做用域鏈那樣。
標識符解析過程與函數聲明週期相關。
函數週期分爲函數建立和函數調用
函數建立
在進入上下文時函數聲明放到變量/活動(VO/AO)對象中。
函數調用
進入上下文建立AO/VO以後,上下文的Scope屬性(變量查找的一個做用域鏈)做以下定義:
Scope = AO|VO + [[Scope]]
一個函數對象被調用的時候,會建立一個活動對象(也就是一個對象),對於每個函數的形參,都命名爲該活動對象的命名屬性,而後將這個活動對象做爲此時的做用域鏈最前端,並將這個函數對象的[[scope]]加入到做用域鏈中。
閉包與詞法做用域直接相關,函數建立時存儲做用域,直到到函數銷燬都不會改變。
實際上,閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的全部局部變量。
閉包容許你從內部函數訪問外部函數的做用域。在JavaScript中,每次在函數調用時都會建立閉包。
要使用閉包,就要在一個函數中定義另外一個函數並暴露該內部函數。若要公開一個內部函數,就要將其返回或傳遞給另外一個函數。即便被外部函數返回以後,內部函數也能夠訪問外部函數做用域中的變量。
在JavaScript中,閉包是用來保護數據隱私的主要機制。閉包是外部範圍和程序其他部分之間的通道。它能夠選擇公開什麼數據,而不公開什麼數據。
function person() { let age = 22; return { getAge: function() { return age; }, setAge: function(v) { age = v; } }; } obj = person(); console.log(obj.getAge()); // 22 obj.setAge(22); console.log(obj.getAge()); // 22 obj.setAge("sueRimn"); console.log(obj.getAge()); // sueRimn
這裏函數返回了一個有兩個函數的對象。由於它們是綁定到局部做用域的對象的屬性,因此它們是閉包。經過getAge
和setAge
,能夠操做age
屬性,但不能直接訪問它。
對象不是產生數據隱私的惟一方法。閉包也能夠用來建立有狀態函數,這些函數的返回值可能會受到其內部狀態的影響,好比:
const name = name => () => name;
因爲保存了來自外部做用域的數據,因此使用閉包建立迭代器至關容易。
function buildContor(i) { var contor = i; var displayContor = function() { console.log(contor++); contor++; }; return displayContor; } var myContor = buildContor(1); myContor(); // 1 myContor(); // 2 myContor(); // 3 // new closure - new outer scope - new contor variable var myOtherContor = buildContor(10); myOtherContor(); // 10 myOtherContor(); // 11 // myContor was not affected myContor(); // 4
上面的buildContor()
函數其實是一個迭代器,每次調用都建立一個新的迭代器,並使用固定的起始索引,而後在每次連續調用迭代器時,返回下一個值。
每次調用其中一個計數器時,經過改變這個變量的值,會改變這個閉包的詞法環境。然而在一個閉包內對變量的修改,不會影響到另一個閉包中的變量。
jQuery(或任何JavaScript)中的事件都是閉包。事件處理程序能夠訪問外部做用域。
$(function() { var contor = 0; $("#Button").click(function() { // 閉包從外部做用域更新變量 contor++; } }
單例對象是在程序執行過程當中只有一個實例的對象。
咱們知道,每次函數調用都會建立一個新的閉包。但若是咱們想阻止外部函數的另外一次調用呢?
很簡單:使用匿名函數。
var person = function () { var age = 22; return { get: function () { return "age: " + age; }, increment: function() { age++; } }; }(); // 注意 單例是該函數回調的結果 console.log(person.get()); // age:22 console.log(person.get()); // age:22 person.increment(); console.log(person.get()); // age:23 person.increment(); console.log(person.get()); // age: 24
這個例子與前面惟一的區別是外部函數是匿名的,它沒有名字。
咱們聲明它並當即調用它,person
對象(即閉包)是訪問其做用域的惟一來源。對於確保建立的age
不會有多個做用域是很是有用的。
若是不是某些特定任務須要使用閉包,在其它函數中建立函數是不明智的,由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。
做用域和閉包若是從單向玻璃理解就很容易。
做用域是在函數定義時產生的,在一個函數內定義任何內部函數,其內部函數稱爲閉包,閉包保留對外部函數中建立的變量的訪問權。
參考:Master the JavaScript Interview: What is a Closure?