我們在面試時,總會碰到一些奇奇怪怪的關於 作用域 的面試題,其實弄清楚原理,萬變不離其宗,大部分的面試題都可以得 '姐'。
所以,今天我們來談談 JavaScript
的 作用域(javascript scope) ,這是老生常談的話題,這裡我們會從 作用域 開始,會延伸到 預解析規則(預編譯) 、 表示式 、 變數提升 、 函式提升 、 匿名函式表示式 、 具名函式表示式 等,徹底搞明白作用域這些事 ?
詳情,可檢視我的部落格 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();
複製程式碼
這裡先揭曉答案:
- 第一個
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
開始執行程式碼之前,有一個 預解析(預編譯) 的過程,這個過程會產生 變數提升 和 函式提升 ,其實整個執行過程可以分為兩部分,方便理解:
- 預解析
這個過程,會把 關鍵字
var
、function
、 引數 提取出來
上面這段程式碼 預解析 的過程是:
// 第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); }
預解析(預編譯) 後的程式碼樣子是這樣的
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);
複製程式碼
這裡先揭曉答案: {% note success %}
- 第一個
alert(a)
彈出undefined
- 第二個
alert(a)
彈出 1 {% endnote %}
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()
, 不要用這種寫法 ,優雅的寫法是 變數名 和 函式名 保持一致foo = function foo()
- 不推薦使用 匿名函式表示式 ,有以下 ? 幾個缺點
- 在追蹤棧中沒函式名,除錯困難
- 如果需要引用自身,只能用非標準的
arguments.callee
(ES5嚴格模式禁用)