
今天我們來談談 JavaScript 的 作用域(javascript scope) ,這是老生常談的話題,這里我們會從 作用域 開始,會延伸到 預解析規(guī)則(預編譯) 、 表達式 、 變量提升 、 函數(shù)提升 、 匿名函數(shù)表達式 、 具名函數(shù)表達式 等,徹底搞明白作用域這些事 ??
詳情,可查閱我的博客 lishaoy.net
變量提升和函數(shù)提升
在開始闡述之前,我們來看一段代碼,看看結(jié)果是什么?
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); }函數(shù)體- 第二個
alter(a)彈出function a(){ alter(4); }函數(shù)體- 第三個
alter(a)彈出 1- 第四個
alter(a)彈出 3- 第五個
alter(a)彈出 3- 最后一行報錯
a is not a function
下面來分析一下這段代碼:
其實在 javascript 開始執(zhí)行代碼之前,有一個 預解析(預編譯) 的過程,這個過程會產(chǎn)生 變量提升 和 函數(shù)提升 ,其實整個執(zhí)行過程可以分為兩部分,方便理解:
-
預解析
這個過程,會把 關鍵字var、function、 參數(shù) 提取出來
上面這段代碼 預解析 的過程是:
// 第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() 函數(shù)調(diào)用
此時這里有4個同名變量 a ,依循規(guī)則是:function 優(yōu)先與 var, 同名的后面覆蓋前面的
因此,a = function a(){ alter(2); } 替換掉下面的2個 ,a = undefineda = 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();
- 執(zhí)行代碼,就是執(zhí)行的這段代碼,依次從上到下執(zhí)行,最后的
a()函數(shù)調(diào)用,這時的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 的作用域只用兩種,一個是全局的,一個是函數(shù)的,也稱為 全局作用域 和 局部作用域 ;局部作用域 可以訪問 全局作用域 。但是 全局作用域 不能訪問 局部作用域
同樣用 預解析(預編譯) 的方法來分析這段代碼
- 預解析(預編譯) 全局作用域
// 第1行,遇到 var 關鍵字,解析到全局的頭部
a = undefined
// 第2行,遇到 function 關鍵字,解析到全局的頭部
fn1 = function fn1(){
alert(a);
var a = 2;
}
// 第3行,沒有遇到關鍵字,不解析
// 第4行,沒有遇到關鍵字,不解析
- 開始執(zhí)行代碼
第1行,遇到表達式 a = 1, a 被賦值成 1 </br>
第6行,遇到函數(shù)調(diào)用 fn1() ,開始 預解析(預編譯) 局部
- 預解析(預編譯) 局部作用域
// 第3行,沒有遇到關鍵字,不解析
// 第4行,遇到 var 關鍵字,解析到局部
a = undefined
- 開始執(zhí)行 局部 代碼
第3行,彈出 undefined
第4行,遇到表達式,把局部 a 改成 2
- 局部執(zhí)行完成,繼續(xù)執(zhí)行全局
第7行,彈出 1 ,因為全局和局部是兩個獨立的作用域
作用域鏈
如果,把上面??代碼,稍作修改
var a = 1;
function fn1(){
alert(a);
a = 2;
}
fn1();
alert(a);
去掉了 function 里的 var ,結(jié)果就會不一樣
這次,輸出的是:
- 第一個
alert彈出 1 - 第二個
alert彈出 2
因為在解析局部是沒有發(fā)現(xiàn)var a,如是在執(zhí)行時,就會去全局查找,找到了全局的a = 1,所以 第一個alert彈出 1 ,而不是undefined,這個就是 作用域連
匿名函數(shù)表達式、具名函數(shù)表達式
在來看看這段代碼??
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
以上代碼包含了 函數(shù)聲明 、 匿名函數(shù)表達式 、 具名函數(shù)表達式 ,匿名函數(shù)表達式 、 具名函數(shù)表達式 是把函數(shù)體賦值給一個變量,因此擁有和變量相同的特性 變量提升 ,而 具名函數(shù)表達式 的函數(shù)名只能在函數(shù)內(nèi)部使用。
了解了這些,再來分析段代碼
- 全局預解析
a = undefined
fn = function fn(){
...
}
執(zhí)行代碼
第1行,遇到表達式,把 a 的值改變成3 </br>
最后行,遇到函數(shù)調(diào)用,重新 預解析 局部局部預解析
// 第4行,遇到 function 關鍵字,解析到局部的頭部
foo = function(){
console.log(1);
}
// 第8行,遇到 var 關鍵字,解析到局部的頭部
foo = undefined
// 第12行,遇到 var 關鍵字,解析到局部的頭部
bar = undefined
由于有兩個同名變量 foo ,遵循 function 優(yōu)先 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();
-
執(zhí)行局部代碼 </br>
第1個foo()輸出的是 1
第2個foo()輸出的是 1
第3個foo()輸出的是 2
第4個foo()輸出的是 2 ,注意這個foo()輸出的是上面foo = function foo() {console.log(2);}的內(nèi)容,因為 具名函數(shù)表達式 的函數(shù)名只能在函數(shù)內(nèi)部使用,在外部無法訪問。
最后的bar()輸出的是 4 ,這里才是輸出function foo() {if(a > 3) return;console.log(++a);foo();}里的內(nèi)容,而且,這個函數(shù)體內(nèi)也有自身的調(diào)用,結(jié)果a變量 +1 ,說明可以調(diào)用,其實,可以用bar.name輸出的就是foo
所以,注意:
bar = function foo(), 不要用這種寫法- 不推薦使用 匿名函數(shù)表達式 ,有以下 ?? 幾個缺點
- 在追蹤棧中沒函數(shù)名,調(diào)試困難
- 如果需要引用自身,只能用非標準的
arguments.callee(ES5嚴格模式禁用)