淺談JavaScript作用域

子木_lsy發表於2018-04-03

我們在面試時,總會碰到一些奇奇怪怪的關於 作用域 的面試題,其實弄清楚原理,萬變不離其宗,大部分的面試題都可以得 '姐'。

所以,今天我們來談談 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 開始執行程式碼之前,有一個 預解析(預編譯) 的過程,這個過程會產生 變數提升函式提升 ,其實整個執行過程可以分為兩部分,方便理解:

  1. 預解析 這個過程,會把 關鍵字 varfunction引數 提取出來

上面這段程式碼 預解析 的過程是:

// 第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); }

預解析(預編譯) 後的程式碼樣子是這樣的

var a = function a(){ alter(4); }
alert(a);
alert(a);
a = 1
alert(a);
a = 3;
alert(a);
alert(a);
a();
複製程式碼
  1. 執行程式碼,就是執行的這段程式碼,依次從上到下執行,最後的 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. 預解析(預編譯) 全域性作用域
// 第1行,遇到 var 關鍵字,解析到全域性的頭部
a = undefined
// 第2行,遇到 function 關鍵字,解析到全域性的頭部
fn1 = function fn1(){
    alert(a);
    var a = 2;
}
// 第3行,沒有遇到關鍵字,不解析
// 第4行,沒有遇到關鍵字,不解析
複製程式碼
  1. 開始執行程式碼

第1行,遇到表示式 a = 1, a 被賦值成 1
第6行,遇到函式呼叫 fn1() ,開始 預解析(預編譯) 區域性

  1. 預解析(預編譯) 區域性作用域
// 第3行,沒有遇到關鍵字,不解析
// 第4行,遇到 var 關鍵字,解析到區域性
a = undefined
複製程式碼
  1. 開始執行 區域性 程式碼

第3行,彈出 undefined 第4行,遇到表示式,把區域性 a 改成 2

  1. 區域性執行完成,繼續執行全域性

第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嚴格模式禁用)

相關文章