談談 JavaScript 的作用域

leiting1998發表於2018-03-28

談談 JavaScript 的作用域

學了好久的 Javascript 慚愧仍然沒有總結完全作用域,今天老夫就來總結一番:

涉及內容:

  1. 全域性作用域
  2. 函式作用域
  3. 塊級作用域
  4. 詞法作用域
  5. 動態作用域 動態作用域跟 this 引用機制相關

全域性作用域:

作用域,是指變數的生命週期(一個變數在哪些範圍內保持一定值)。

全域性變數:

生命週期將存在於整個程式之內。

能被程式中任何函式或者方法訪問。

在 JavaScript 內預設是可以被修改的。

全域性變數,雖然好用,但是是非常可怕的,這是所有程式設計師公認的事實。

顯式宣告:

帶有關鍵字 var 的宣告;

<script type="text/javascript">

    var testValue = 123;

    var testFunc = function () { console.log('just test') };

    /**---------全域性變數會掛載到 window 物件上------------**/

    console.log(window.testFunc)		// ƒ () { console.log('just test') }

    console.log(window.testValue)		// 123
    
</script>
複製程式碼

其實,我們寫的函式如果不經過封裝,也會是全域性變數,他的生命週期也就是全域性作用域;

隱式宣告:

不帶有宣告關鍵字的變數,JS 會預設幫你宣告一個全域性變數!!!

<script type="text/javascript">

    function foo(value) {

      result = value + 1;	 // 沒有用 var 修飾

      return result;
    };

    foo(123);				// 124
    
    console.log(window.result);			// 124 <=  掛在了 window全域性物件上 
    
</script>
複製程式碼

現在,變數 result 被掛載到 window 物件上了!!!

函式作用域:

函式作用域內,對外是封閉的,從外層的作用域無法直接訪問函式內部的作用域!!!


function bar() {
  var testValue = 'inner';
}

console.log(testValue);		// 報錯:ReferenceError: testValue is not defined
複製程式碼
通過 return 訪問函式內部變數:
function bar(value) {
  var testValue = 'inner';
  
  return testValue + value;
}

console.log(bar('fun'));		// "innerfun"
複製程式碼

函式就像一個工廠,我們輸入一些東西,它在內部加工,然後給我們一個加工產物;

通過 閉包 訪問函式內部變數:
function bar(value) {
  var testValue = 'inner';
  
  var rusult = testValue + value;
  
  function innser() {
     return rusult;
  };
  
  return innser();
}

console.log(bar('fun'));		// "innerfun"
複製程式碼

關於閉包,我不會在這篇文章過多描述,因為,想要描述閉包,本身需要跟本文章一樣的長度;

立即執行函式:

這是個很實用的函式,很多庫都用它分離全域性作用域,形成一個單獨的函式作用域;

<script type="text/javascript">

    (function() {

      var testValue = 123;

      var testFunc = function () { console.log('just test'); };

    })();

    console.log(window.testValue);		// undefined
    
    console.log(window.testFunc);		// undefined
    
</script>
複製程式碼

它能夠自動執行 (function() { //... })() 裡面包裹的內容,能夠很好地消除全域性變數的影響;

塊級作用域:

在 ES6 之前,是沒有塊級作用域的概念的。如果你有 C++ 或者 Java 經驗,想必你對塊級作用域並不陌生;

for(var i = 0; i < 5; i++) {
  // ...
}

console.log(i)				// 5
複製程式碼

很明顯,用 var 關鍵字宣告的變數,在 for 迴圈之後仍然被儲存這個作用域裡;

這可以說明: for() { }仍然在,全域性作用域裡,並沒有產生像函式作用域一樣的封閉效果;

如果想要實現 塊級作用域 那麼我們需要用 let 關鍵字宣告!!!

for(let i = 0; i < 5; i++) {
  // ...
}

console.log(i)				// 報錯:ReferenceError: i is not defined
複製程式碼

for 迴圈執行完畢之後 i 變數就被釋放了,它已經消失了!!!

同樣能形成塊級作用域的還有 const 關鍵字:

if (true) {
  const a = 'inner';
}

console.log(a);				// 報錯:ReferenceError: a is not defined
複製程式碼

letconst 關鍵字,建立塊級作用域的條件是必須有一個 { } 包裹:

{
  let a = 'inner';
}
  
if (true) {
   let b = 'inner'; 
}

var i = 0;

// ......
複製程式碼

不要小看塊級作用域,它能幫你做很多事情,舉個栗子:

舉一個面試中常見的例子:

for(var i = 0; i < 5; i++) {
  setTimeout(function() {
     console.log(i);			// 5 5 5 5 5
  }, 200);
};
複製程式碼

這幾乎是作用域的必考題目,你會覺得這種結果很奇怪,但是事實就是這麼發生了;

這裡的 i 是在全域性作用域裡面的,只存在 1 個值,等到回撥函式執行時,用詞法作用域捕獲的 i 就只能是 5;

因為這個迴圈計算的 i 值在回撥函式結束之前就已經執行到 5 了;我們應該如何讓它恢復正常呢???

解法1:呼叫函式,建立函式作用域:

for(var i = 0; i < 5; i++) {
  abc(i);
};

function abc(i) {
  setTimeout(function() {
     console.log(i);			// 0 1 2 3 4 
  }, 200); 
}

複製程式碼

這裡相當於建立了5個函式作用域來儲存,我們的 i 值;

解法2:採用立即執行函式,建立函式作用域;

for(var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 200);
  })(i);
};
複製程式碼

原理同上,只不過換成了自動執行這個函式罷了,這裡儲存了 5 次 i 的值;

解法3:let 建立塊級作用域

for(let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(i);
    }, 200);
};
複製程式碼

詞法作用域:

詞法作用域是指一個變數的可見性,及其文字表述的模擬值(《JavaScript函數語言程式設計》);

聽起來,十分地晦澀,不過將程式碼拿來分析就非常淺顯易懂了;

testValue = 'outer';

function afun() {
  var testValue = 'middle';
  
  console.log(testValue);		// "middle"
  
  function innerFun() {
    var testValue = 'inner';
    
    console.log(testValue);		// "inner"
  }
  
  return innerFun();
}

afun();

console.log(testValue);			// "outer"
複製程式碼

當我們要使用宣告的變數時:JS引擎總會從最近的一個域,向外層域查詢;

談談 JavaScript 的作用域

再舉一個一個實際的例子:

var testValue = 'outer';

function foo() {
  console.log(testValue);		// "outer"
}

function bar() {
  var testValue = 'inner';
  
  foo();
}

bar();
複製程式碼

顯然,當 JS 引擎查詢這個變數時,發現全域性的 testValue 離得更近一些,這恰好和 動態作用域 相反;

談談 JavaScript 的作用域

如上圖所示,下面將講述與 詞法作用域相反的動態作用域;

動態作用域:

在程式設計中,最容易被低估和濫用的概念就是動態作用域(《JavaScript函數語言程式設計》)。

在 JavaScript 中的僅存的應用動態作用域的地方:this 引用,所以這是一個大坑!!!!!

動態作用域,作用域是基於呼叫棧的,而不是程式碼中的作用域巢狀;

作用域巢狀,有詞法作用域一樣的特性,查詢變數時,總是尋找最近的作用域;

同樣是,詞法作用域,例子2,同一份程式碼,如果 是動態作用域:

var testValue = 'outer';

function foo() {
  console.log(testValue);		// "inner"
}

function bar() {
  var testValue = 'inner';
  
  foo();
}

bar();
複製程式碼

當然,JavaScript 除了this之外,其他,都是根據詞法作用域查詢!!!

談談 JavaScript 的作用域

為什麼要理解動態作用域呢?因為,這能讓你更好地學習 this 引用!!!

參考和鳴謝:

  • 《你不知道的JavaScript》
  • 《JavaScript函數語言程式設計》

首先,感謝這兩本書的作者,我也算是結合自己的例項和理解,好好地總結了一下作用域;

相關文章