淺談JavaScript做用域

javascriptScope

咱們在面試時,總會碰到一些奇奇怪怪的關於 做用域 的面試題,其實弄清楚原理,萬變不離其宗,大部分的面試題均可以得 ‘姐’。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

  • 預解析

這個過程,會把 關鍵字 varfunction參數 提取出來調試

上面這段代碼 預解析 的過程是: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 = undefineda = 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嚴格模式禁用)
相關文章
相關標籤/搜索