全部的編程語言均可以存儲,訪問,修改變量。可是這些變量如何存儲,程序如何找到而且可以使用它們?這些問題須要設計一套規則,這套規則就被稱爲咱們所熟知的做用域
。編程
在介紹做用域以前,先來了解JavaScript
這門語言,一般百科的說法是JavaScript
是一種高級的,解釋執行的編程語言。但事實上它也是一門編譯語言。也須要經歷傳統編譯語言的步驟。詞法分析
、語法分析
、代碼生成
這三個步驟統稱爲「編譯」。對於JavaScript
來講,大部分狀況下編譯發生在代碼執行前的幾微秒的時間內。性能優化
這裏就要說到JavaScript
的工做原理,JavaScript
工做時由引擎,編譯器以及做用域共同完成。例如var a = 1;
,咱們來簡單分析一下。編程語言
var a
編譯器首先詢問做用域是否已經有一個該名稱的變量存在同一個做用域的集合中。若是有,則編譯器忽略該聲明,繼續編譯;反之它會要求做用域在當前做用域的集合中聲明一個新的變量a
。a=1
這個賦值操做。引擎運行時會首先詢問做用域,在當前的做用域集合中是否存在一個a
的變量。若是是,引擎就會使用這個變量;反之引擎繼續查找該變量。a
變量,就會將1
賦值給它。反之引擎就會拋出一個異常。當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量或抵達最外層的做用域(全局做用域)爲止。函數
function foo(b) {
return a + b;
}
var a = 1;
foo(2); // 3
複製代碼
引擎會在foo的做用域中尋找a,沒有找到該變量,繼續向上層尋找也就是全局做用域,而後在全局做用域中尋找到變量a。引擎在遍歷過程當中,會產生一個做用域鏈。做用域鏈的用途,確保變量和函數有規則的訪問。性能
詞法做用域就是定義在詞法階段階段的做用域。直觀的說法就是詞法做用域是由你寫代碼時將變量和塊做用域寫在哪裏來決定的。定義比較抽象,這裏舉例說明。學習
function foo(a) {
var b = a + 1;
function bar(b) {
var c = b + 1;
console.log(a, b, c);
}
bar(b);
}
var a = 1;
foo(a); //1, 2, 3
複製代碼
爲了幫助理解,能夠想象成逐級包含的氣泡。如圖所示。 優化
當引擎console.log(a, b, c)
執行時。它首先從最內部的做用域,也就是bar()
函數的做用域氣泡開始查找。引擎沒法在找到a
,所以會繼續遍歷到上層foo()
函數的做用域查找。仍是沒有找到a
,引擎繼續向上遍歷查找,在全局做用域中找到了a
,引擎就會使用這個引用。同理b
和c
同樣引擎重複a
的方式進行查找。ui
做用域查找會在找到第一個匹配的標識符時中止。spa
此外還有2種修改詞法做用域的方法eval()
和with
。使用這兩個方法會對性能產生影響。由於JavaScript
引擎會在編譯階段進行性能優化,其中一些優化依賴代碼的詞法分析,若是使用eval()
和with
其中的代碼沒法獲得優化。這裏就不展開說明官方文檔都有很詳細的說明eval(),with。設計
JavaScript
中最多見的就是基於函數的做用域,每聲明一個函數都會爲其自身建立一個做用域氣泡。
function foo(a) {
var b = 2;
function bar() {
var c = 3;
}
}
複製代碼
這個代碼片斷中,foo()
的做用域中包含了標識符a
、b
、c
和bar
,全局做用域中包含一個標識符foo
。因爲標識符a
、b
、c
和bar
都屬於foo()
的做用域,所以沒法在外部對它們進行訪問。也就是說在全局做用域中進行訪問,下面代碼會致使錯誤:
console.log(a, b, c);
bar();
複製代碼
函數做用域的含義是指,屬於這個函數的標識符均可以在整個函數的範圍內使用及複用。
對於函數的認知先聲明一個函數,而後向裏面添加代碼。若是反過來,從代碼中挑選一個片斷,而後用函數聲明對它進行包裝。實際就是把這段代碼內部「隱藏」起來。而且這個代碼片斷擁有本身的做用域。在實際開發中有不少狀況也會使用這種做用域的隱藏方法。好比某個模塊或API設計,只對外暴露方法和接口,不暴露內部的實現方法和變量。例如:
function foo(a) {
b = a + bar(1);
console.log(b * 2);
}
function bar(c) {
return c + 1;
}
var b;
foo(3);
複製代碼
在這段代碼中變量b
和函數bar()
應該是函數foo()
內部的具體實現內容,可是外部做用域也有訪問 b
和bar()
的權限。由於它們有可能被有意或無心地以非預期的方式使用。這裏須要更合理的設計,例如:
function foo(a) {
function bar(c) {
return c + 1;
}
var b;
b = a + bar(1);
console.log(b * 2);
}
foo(3);
複製代碼
如今變量b
和函數bar()
都沒法從外部直接被訪問,只能在foo()
中使用,功能和結果都沒有受影響。設計良好的軟件都會將一些內容私有化。
隱藏內部實現的另外一個好處就是能夠避免同名標識符的衝突,這在軟件設計中很常見,兩個標識符可能具備相同的名字可是用途卻徹底不同。無心間致使命名衝突,變量的值被意外覆蓋。例如:
function foo() {
function bar(a) {
i = 5; //修改循環做用域i
console.log(a + i);
}
var i = 1;
while(i < 10) {
bar(i); //無限循環了
i ++;
}
}
foo();
複製代碼
bar()
內部的賦值語句i = 5
意外地覆蓋了聲明在foo()
函數中的i
,致使無限循環。bar()
內部須要聲明一個本地變量來使用或者採用一個徹底不一樣的標識符,例如var j = 5
,這樣就能避免變量衝突。
儘管大部分狀況都廣泛使用函數做用域,但也存在塊做用域。
with
這裏不作詳細說明,with能夠查看mdn官方文檔。
try {
empty(); // 執行一個不存在的方法來拋出異常
} catch (error) {
console.log(error); // 可以正常執行!
}
console.log(error); // Uncaught ReferenceError: error is not defined
複製代碼
try/catch
中catch
語句會建立一個塊做用域,其中聲明的變量只能在catch
中訪問。
ES6引入了新的let
關鍵字,let
語句聲明一個塊級做用域的本地變量,而且可選的將其初始化爲一個值。let
關鍵字能夠將變量綁定到所在的任意做用域中(一般用{...})。例如:
function letTest() {
let x = 1;
if (true) {
let x = 2; // 不一樣的變量
console.log(x); // 2
}
console.log(x); // 1
}
letTest();
複製代碼
ES6還引入了const
關鍵字,聲明一個塊級做用域常量,其值是固定不可更改的,常量的值不能經過從新賦值來改變,而且不能從新聲明。試圖修改值的操做都會報錯。
function constTest() {
if(true) {
var a = 1;
const b = 2;
a = 3;
b = 4; // Uncaught TypeError: Assignment to constant variable.
}
console.log(a);
console.log(b);// Uncaught ReferenceError: b is not defined
}
constTest();
複製代碼
學習JavaScript也有幾年了,一直都是很零碎的學習。寫此文的目的一方面是寫給本身看的筆記,一方面也是對知識的總結。