咱們在面試時,總會碰到一些奇奇怪怪的關於 做用域 的面試題,其實弄清楚原理,萬變不離其宗,大部分的面試題均可以得 ‘姐’。javascript
因此,今天咱們來談談 JavaScript
的 做用域(javascript scope) ,這是老生常談的話題,這裏咱們會從 做用域 開始,會延伸到 預解析規則(預編譯) 、 表達式 、 變量提高 、 函數提高 、 匿名函數表達式 、 具名函數表達式 等,完全搞明白做用域這些事 ?java
詳情,可查看個人博客 lishaoy.net面試
在開始闡述以前,咱們來看一段代碼,看看結果是什麼?函數
alert(a); function a(){ alter(2); } alert(a); var a = 1 alert(a); var a = 3; alert(a); function a(){ alter(4); } alert(a); a();
這裏先揭曉答案:spa
- 第一個
alert(a)
彈出function a(){ alter(4); }
函數體- 第二個
alter(a)
彈出function a(){ alter(4); }
函數體- 第三個
alter(a)
彈出 1- 第四個
alter(a)
彈出 3- 第五個
alter(a)
彈出 3- 最後一行報錯
a is not a function
下面來分析一下這段代碼:
其實在 javascript
開始執行代碼以前,有一個 預解析(預編譯) 的過程,這個過程會產生 變量提高 和 函數提高 ,其實整個執行過程能夠分爲兩部分,方便理解:.net
這個過程,會把 關鍵字 var
、 function
、 參數 提取出來調試
上面這段代碼 預解析 的過程是:code
// 第1行,沒有關鍵字 , 不解析 // 第2行,遇到 function 關鍵字,解析到全局的頭部 a = function a(){ alter(2); } // 第3行,沒有關鍵字 , 不解析 // 第4行,遇到關鍵字 var , 解析到全局的頭部 a = undefined // 第5行,沒有關鍵字 , 不解析 // 第6行,遇到關鍵字 var , 解析到全局的頭部 a = undefined // 第8行,遇到 function 關鍵字,解析到全局的頭部 a = function a(){ alter(4); } // 第9行,沒有關鍵字 , 不解析 // 第10行,a() 函數調用
此時這裏有4個同名變量 a ,依循規則是:function
優先與 var
, 同名的後面覆蓋前面的
所以,a = function a(){ alter(2); }
替換掉下面的2個 ,a = undefined
a = function a(){ alter(4); }
又替換掉 ,最終只剩下 a = function a(){ alter(2); }
a = function a(){ alter(4); }
ip
預解析(預編譯) 後的代碼樣子是這樣的作用域
var a = function a(){ alter(4); } alert(a); alert(a); a = 1 alert(a); a = 3; alert(a); alert(a); a();
就是執行的這段代碼,依次從上到下執行,最後的 a()
函數調用,這時的 a
已被 表達式 賦值成 3 ,而報錯 a is not a function
再看這段代碼
var a = 1; function fn1(){ alert(a); var a = 2; } fn1(); alert(a);
這裏先揭曉答案:
- 第一個
alert(a)
彈出undefined
- 第二個
alert(a)
彈出 1
JavaScript
的做用域只用兩種,一個是全局的,一個是函數的,也稱爲 全局做用域 和 局部做用域 ;局部做用域 能夠訪問 全局做用域 。可是 全局做用域 不能訪問 局部做用域
一樣用 預解析(預編譯) 的方法來分析這段代碼
// 第1行,遇到 var 關鍵字,解析到全局的頭部 a = undefined // 第2行,遇到 function 關鍵字,解析到全局的頭部 fn1 = function fn1(){ alert(a); var a = 2; } // 第3行,沒有遇到關鍵字,不解析 // 第4行,沒有遇到關鍵字,不解析
第1行,遇到表達式 a = 1
, a 被賦值成 1
第6行,遇到函數調用 fn1()
,開始 預解析(預編譯) 局部
// 第3行,沒有遇到關鍵字,不解析 // 第4行,遇到 var 關鍵字,解析到局部 a = undefined
第3行,彈出 undefined
第4行,遇到表達式,把局部 a 改爲 2
第7行,彈出 1 ,由於全局和局部是兩個獨立的做用域
若是,把上面?代碼,稍做修改
var a = 1; function fn1(){ alert(a); a = 2; } fn1(); alert(a);
去掉了 function
裏的 var
,結果就會不同
此次,輸出的是:
alert
彈出 1 alert
彈出 2 由於在解析局部是沒有發現 var a
,如是在執行時,就會去全局查找,找到了全局的 a = 1
,因此 第一個 alert
彈出 1 ,而不是 undefined
,這個就是 做用域連
在來看看這段代碼?
var a = 3; function fn() { foo(); function foo() { console.log(1); } foo(); var foo = function() { console.log(2); }; foo(); var bar = function foo() { if(a > 3) return; console.log(++a); foo(); }; foo(); bar(); } fn();
先揭曉答案:
- 第1個
foo()
輸出的是 1- 第2個
foo()
輸出的是 1- 第3個
foo()
輸出的是 2- 第4個
foo()
輸出的是 2- 最後的
bar()
輸出的是 4
以上代碼包含了 函數聲明 、 匿名函數表達式 、 具名函數表達式 ,匿名函數表達式 、 具名函數表達式 是把函數體賦值給一個變量,所以擁有和變量相同的特性 變量提高 ,而 具名函數表達式 的函數名只能在函數內部使用。
瞭解了這些,再來分析段代碼
a = undefined fn = function fn(){ ... }
第1行,遇到表達式,把 a 的值改變成3
最後行,遇到函數調用,從新 預解析 局部
// 第4行,遇到 function 關鍵字,解析到局部的頭部 foo = function(){ console.log(1); } // 第8行,遇到 var 關鍵字,解析到局部的頭部 foo = undefined // 第12行,遇到 var 關鍵字,解析到局部的頭部 bar = undefined
因爲有兩個同名變量 foo
,遵循 function
優先 var
所以, 被幹掉foo = undefined
局部預解析 完以後的代碼應該是這個樣子?
var a = 3 function fn() { var foo = function foo() { console.log(1); } var bar; foo(); foo(); foo = function foo() { console.log(2); }; foo(); bar = function foo() { if(a > 3) return; console.log(++a); foo(); }; foo(); bar(); } fn();
第1個 foo()
輸出的是 1
第2個 foo()
輸出的是 1
第3個 foo()
輸出的是 2
第4個 foo()
輸出的是 2
注意 這個foo()
輸出的是上面foo = function foo() {console.log(2);}
的內容,由於 具名函數表達式 的函數名只能在函數內部使用,在外部沒法訪問。
最後的 bar()
輸出的是 4
這裏纔是輸出function foo() {if(a > 3) return;console.log(++a);foo();}
裏的內容,並且,這個函數體內也有自身的調用,結果a
變量 +1 ,說明能夠調用,其實,能夠用bar.name
輸出的就是foo
因此,注意:
bar = function foo()
, 不要用這種寫法不推薦使用 匿名函數表達式 ,有如下 ? 幾個缺點
- 在追蹤棧中沒函數名,調試困難
- 若是須要引用自身,只能用非標準的
arguments.callee
(ES5嚴格模式禁用)