前端學習(34)~js學習(十一):作用域和變數提升

  • 2020 年 3 月 18 日
  • 筆記

作用域:通俗來講,作用域是一個變數或函數的作用範圍。作用域在函數定義時,就已經確定了。

目的:為了提高程式的可靠性,同時減少命名衝突。

作用域的分類

  • 全局作用域:作用於整個 script 標籤內部,或者作用域一個獨立的 JS 文件。
  • 函數作用域(局部作用域):作用於函數內的程式碼環境。

作用域的訪問關係

在內部作用域中可以訪問到外部作用域的變數,在外部作用域中無法訪問到內部作用域的變數。

程式碼舉例:

var a = 'aaa';  function foo() {      var b = 'bbb';      console.log(a); // 列印結果:aaa。說明 內層作用域 可以訪問 外層作用域 里的變數  }    foo();  console.log(b); // 報錯:Uncaught ReferenceError: b is not defined。說明 外層作用域 無法訪問 內層作用域 里的變數

變數的作用域

根據作用域的不同,變數可以分為兩類:全局變數、布局變數。

全局變數:

  • 在全局作用域下聲明的變數,叫「全局變數」。在全局作用域的任何一地方,都可以訪問這個變數。
  • 在全局作用域下,使用 var 聲明的變數是全局變數。
  • 特殊情況:在函數內不使用 var 聲明的變數也是全局變數(不建議這麼用)。

局部變數:

  • 定義在函數作用域的變數,叫「局部變數」。
  • 在函數內部,使用 var 聲明的變數是局部變數。
  • 函數的形參也是屬於局部變數。

從執行效率來看全局變數和局部變數:

  • 全局變數:只有瀏覽器關閉時才會被銷毀,比較占記憶體。
  • 局部變數:當其所在的程式碼塊運行結束後,就會被銷毀,比較節約記憶體空間。

作用域的上下級關係

當在函數作用域操作一個變數時,它會先在自身作用域中尋找,如果有就直接使用(就近原則)。如果沒有則向上一級作用域中尋找,直到找到全局作用域;如果全局作用域中依然沒有找到,則會報錯 ReferenceError。

在函數中要訪問全局變數可以使用window對象。(比如說,全局作用域和函數作用域都定義了變數a,如果想訪問全局變數,可以使用window.a)

全局作用域

直接編寫在script標籤中的JS程式碼,都在全局作用域。

  • 全局作用域在頁面打開時創建,在頁面關閉時銷毀。
  • 在全局作用域中有一個全局對象window,它代表的是一個瀏覽器的窗口,由瀏覽器創建,我們可以直接使用。

在全局作用域中:

  • 創建的變數都會作為window對象的屬性保存。比如在全局作用域內寫 var a = 100,這裡的 a 等價於 window.a。
  • 創建的函數都會作為window對象的方法保存。

變數的聲明提前(變數提升)

使用var關鍵字聲明的變數( 比如 var a = 1),會在所有的程式碼執行之前被聲明(但是不會賦值),但是如果聲明變數時不是用var關鍵字(比如直接寫a = 1),則變數不會被聲明提前。

舉例1:

    console.log(a);      var a = 123;

列印結果:undefined。注意,列印結果並沒有報錯,而是 undefined,說明變數 a 被提前聲明了,只是尚未被賦值。

舉例2:

    console.log(a);      a = 123;   //此時a相當於window.a

程式會報錯:Uncaught ReferenceError: a is not defined。

舉例3:

    a = 123;   //此時a相當於window.a      console.log(a);

列印結果:123。

舉例4:

foo();    function foo() {      if (false) {          var i = 123;      }      console.log(i);  }

列印結果:undefined。注意,列印結果並沒有報錯,而是 undefined。這個例子,再次說明了:變數 i 在函數執行前,就被提前聲明了,只是尚未被賦值。

函數的聲明提前

函數聲明:

使用函數聲明的形式創建的函數function foo(){},會被聲明提前。

也就是說,整個函數會在所有的程式碼執行之前就被創建完成。所以,在程式碼順序里,我們可以先調用函數,再定義函數。

程式碼舉例:

    fn1();  // 雖然 函數 fn1 的定義是在後面,但是因為被提前聲明了, 所以此處可以調用函數        function fn1() {          console.log('我是函數 fn1');      }

函數表達式:

使用函數表達式創建的函數var foo = function(){},不會被聲明提前,所以不能在聲明前調用。

很好理解,因為此時foo被聲明了(這裡只是變數聲明),且為undefined,並沒有把 function(){} 賦值給 foo。

所以說,下面的例子,會報錯:

函數作用域

提醒1:在函數作用域中,也有聲明提前的特性:

  • 函數中,使用var關鍵字聲明的變數,會在函數中所有的程式碼執行之前被聲明。
  • 函數中,沒有var聲明的變數都是全局變數,而且並不會提前聲明。

舉例:

    var a = 1;        function foo() {          console.log(a);          a = 2;     // 此處的a相當於window.a      }        foo();      console.log(a);   //列印結果是2

上方程式碼中,執行foo()後,函數裡面的列印結果是1。如果去掉第一行程式碼,執行foo()後,函數裡面的列印結果是Uncaught ReferenceError: a is not defined。

提醒2:定義形參就相當於在函數作用域中聲明了變數。

    function fun6(e) { // 這個函數中,因為有了形參 e,此時就相當於在函數內部的第一行程式碼里,寫了 var e;          console.log(e);      }        fun6();  //列印結果為 undefined      fun6(123);//列印結果為123

JavaScript 沒有塊級作用域(ES6之前)

在其他程式語言中(如 Java、C#等),存在塊級作用域,由{}包括起來。比如在 Java 語言中,if 語句里創建的變數,只能在if語句內部使用:

if(true){      int num = 123;      system.out.print(num); // 123  }  system.out.print(num); // 報錯

但是,在 JS 中沒有塊級作用域(ES6之前)。舉例如下:

if(true){  var num = 123;      console.log(123); //123  }    console.log(123); //123(可以正常列印)

作用域鏈

引入:

  • 只要是程式碼,就至少有一個作用域
  • 寫在函數內部的局部作用域
  • 如果函數中還有函數,那麼在這個作用域中就又可以誕生一個作用域

基於上面幾條內容,我們可以得出作用域鏈的概念。

作用域鏈:內部函數訪問外部函數的變數,採用的是鏈式查找的方式來決定取哪個值,這種結構稱之為作用域鏈。查找時,採用的是就近原則。

程式碼舉例:

var num = 10;    function fn() {      // 外部函數      var num = 20;        function fun() {          // 內部函數          console.log(num);      }      fun();  }  fn();

列印結果:20。

Tags: