­

前端學習(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。