理解Javascript的變量提高

前言

本文2922字,閱讀大約須要8分鐘。

總括: 什麼是變量提高,使用var,let,const,function,class聲明的變量函數類在變量提高的時候都有什麼區別。javascript

要麼庸俗,要麼孤獨。前端

正文

Javascript中的變量提高說的是在程序中能夠在變量聲明以前就進行使用:java

console.log(a); // undefined
var a = 1;

能夠看到,在變量a聲明以前咱們能夠正常調用a,代碼的實際的表現更像是這樣的:數組

var a;
console.log(a); // undefined
a = 1;

但實際上,代碼並無被改變,上面的代碼只是咱們猜想的,其實Javascript引擎在執行這幾行代碼的時候並無移動或是改變代碼的結果。到底發生了什麼呢?bash

變量提高

在代碼的編譯期間,即代碼真正執行的瞬息之間,引擎會將代碼塊中全部的變量聲明和函數聲明都記錄下來。這些函數聲明和變量聲明都會被記錄在一個名爲詞法環境的數據結構中。詞法環境是Javascript引擎中一種記錄變量和函數聲明的數據結構,它會被直接保存在內存中。因此,上面的console.log(a)能夠正常執行。數據結構

什麼是詞法環境

所謂詞法環境就是一種標識符—變量映射的結構(這裏的標識符指的是變量/函數的名字,變量是對實際對象[包含函數和數組類型的對象]或基礎數據類型的引用)。函數

簡單地說,詞法環境是Javascript引擎用來存儲變量和對象引用的地方。學習

詞法環境的結構用僞代碼表示以下:this

LexicalEnvironment = {
  Identifier:  <value>,
  Identifier:  <function object>
}

關於詞法環境更多的瞭解能夠看博主以前的譯文:理解Javascript中的執行上下文和執行棧spa

瞭解了詞法環境接下來讓我依次看下使用var,const,let,function,class聲明的變量或函數的狀況。

function聲明提高

helloWorld();  // 打印 'Hello World!'
function helloWorld(){
  console.log('Hello World!');
}

咱們已經知道了,函數聲明會在編譯階段就會被記錄在詞法環境中而且保存在內存中,所以咱們能夠在函數進行實際聲明以前對該函數進行訪問。

上面函數聲明保存在詞法環境中像下面這樣:

lexicalEnvironment = {
  helloWorld: < func >
}

因此在代碼執行階段,當Javascript引擎碰到helloWorld()這行代碼,會在詞法環境中尋找,而後找到這個函數並執行它。

函數表達式

注意,只有函數聲明纔會被直接提高,使用函數表達式聲明的函數不會被提高,看下面代碼:

helloWorld();  // TypeError: helloWorld is not a function
var helloWorld = function(){
  console.log('Hello World!');
}

如上,代碼報錯了。使用var聲明的helloWorld是個變量,並非函數,Javascript引擎只會把它當成普通的變量來處理,而不會在詞法環境中給它賦值。

保存在詞法環境中像下面這樣:

lexicalEnvironment = {
  helloWorld: undefined
}

上面的代碼要想能夠正常運行改寫以下便可:

var helloWorld = function(){
  console.log('Hello World!');
}
helloWorld();  // 打印 'Hello World!'

var變量提高

看一個使用var聲明變量的例子:

console.log(a); // 打印 'undefined'
var a = 3;

若是按上面function函數聲明的方式去理解,這裏應該打印3,但實際上打印了undefined

請記住:所謂的聲明提高只是在編譯階段Javascript引擎將函數聲明和變量聲明存儲在詞法環境中,但不會給它們賦值。等到了執行階段,真正執行到賦值那一行的時候,詞法環境纔會更新。

但上面的代碼爲何打印了undefined呢?

Javascript引擎會在編譯階段將使用var聲明的變量保存在詞法環境中,並將它初始化爲undefined。到了執行階段,等執行到賦值那一行代碼的時候,詞法環境中變量的值纔會被更新。

因此上面代碼的詞法環境初始化像下面這樣:

lexicalEnvironment = {
  a: undefined
}

這也解釋了爲何前面使用函數表達式聲明的函數執行會報錯,爲何上面的代碼會打印undefined。當代碼執行到var a = 3;這行代碼的時候,詞法環境中a的值就會被更新,此時詞法環境會被更新以下:

lexicalEnvironment = {
  a: 3
}

let和const變量提高

看一個使用let聲明變量的例子:

console.log(a);
let a = 3;

輸出:

Uncaught ReferenceError: Cannot access 'a' before initialization

再看一個使用const聲明變量的例子:

console.log(b);
const b = 1;

輸出:

Uncaught ReferenceError: Cannot access 'b' before initialization

var不一樣,相同結構的代碼換成let或是const都直接報錯了。

難道使用letconst聲明的變量不存在變量提高的狀況麼?

實際上,在Javascript中全部聲明的變量(var,const,let,function,class)都存在變量提高的狀況。使用var聲明的變量,在詞法環境中會被初始化爲undefined,但用letconst聲明的變量並不會被初始化。

使用letconst聲明的變量只有在執行到賦值那行代碼的時候纔會真正給他賦值,這也意味着在執行到變量聲明的那行代碼以前訪問那個變量都會報錯,這就是咱們常說的暫時性死區(TDZ)。即在變量聲明以前都不能對變量進行訪問。

當執行到變量聲明的那一行的時候,可是仍然沒有賦值,那麼使用let聲明的變量就會被初始化爲undefined;使用const聲明的變量就會報錯; 看實際的例子:

let a;
console.log(a); // 輸出 undefined
a = 5;

在代碼編譯階段,Javascript引擎會把變量a存儲在詞法環境中,並把a保持在未初始化的狀態。此時詞法環境像下面這樣:

lexicalEnvironment = {
  a: <uninitialized>
}

此時若是嘗試訪問變量a或是b,Javascript引擎會在詞法環境中找到該變量,但此時變量處於未初始化的狀態,所以會拋出一個引用錯誤。

而後在執行階段,Javascript引擎執行到賦值(專業點叫詞法綁定)那一行的時候,會評估被賦值的值,若是沒有被賦值,只是簡單的聲明,此時就會給let聲明的變量賦值爲undefined;此時詞法環境像下面這樣:

lexicalEnvironment = {
  a: undefined
}

當執行到a = 5這一行的時候,詞法環境再次更新:

lexicalEnvironment = {
  a: 5
}

再看下使用const聲明代碼的狀況:

let a;
console.log(a);
a = 5;
const b;
console.log(b);

輸出:

Uncaught SyntaxError: Missing initializer in const declaration

上面代碼直接報錯,a的值也沒有打印,直接報錯,實際上是代碼在編譯階段就已經報錯了,壓根沒執行到console.log(a);這一行代碼。

注意:在函數中,只要是能在變量聲明以後引用該變量就不會報錯。

什麼意思呢?看以下代碼:

function foo () {
  console.log(a);
}
let a = 20;
foo(); // 打印 20

但下面代碼就會報錯:

function foo () {
  console.log(a);
}
foo();
let a = 20; // 報錯: Uncaught ReferenceError: Cannot access 'a' before initialization

這裏報錯的緣由須要結合Javascript中的執行上下文和執行棧才能理解,由於此時全局執行上下文中詞法環境中保存的變量a處於未初始化的狀態,調用foo函數,建立了一個函數執行上下文,而後函數foo執行過程對全局執行上下文的變量a進行訪問,但a還處於未初始化的狀態(此時let a = 20尚未執行)。所以報錯。

這裏須要糾正一個誤區,就是letconst聲明的變量只有暫時性死區,不存在變量提高,實際上是不對的,舉個例子證實理解一下:

let a = 1;
{
  console.log(a);
  let a = 2;
}

上面的代碼會被報錯:

Uncaught ReferenceError: Cannot access 'a' before initialization

若是不存在變量提高,理論上不會報錯纔對。

class聲明提高

letconst相似,使用class聲明的類也會被提高,而後這個類聲明會被保存在詞法環境中但處於未初始化的狀態,直到執行到變量賦值那一行代碼,纔會被初始化。另外,class聲明的類同樣存在暫時性死區(TDZ)。看例子:

let peter = new Person('Peter', 25); 
console.log(peter);
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

打印:

Uncaught ReferenceError: Cannot access 'Person' before initialization

改寫以下就能夠正常運行了:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

上面代碼在編譯階段,詞法環境像這樣:

lexicalEnvironment = {
  Person: <uninitialized>
}

而後執行到class聲明的那一行代碼,此時詞法環境像下面這樣:

lexicalEnvironment = {
  Person: <Person object>
}

注意:使用構造函數實例化對象並不會報錯:

let peter = new Person('Peter', 25);
console.log(peter);
function Person(name, age) {

    this.name = name;
    this.age = age;
}
// Person { name: 'Peter', age: 25 }

上面代碼正常運行。

類表達式

和函數表達式同樣,類表達式也同樣會被提高,好比:

let peter = new Person('Peter', 25);
console.log(peter);
let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

報錯:

Uncaught ReferenceError: Cannot access 'Person' before initialization

要想正常運行,改寫以下便可:

let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

也就是說無論是函數表達式仍是類表達式遵循的規則和變量聲明是同樣的。

結論

無論是var,const,let,function,class聲明的變量仍是函數都存在變量提高的狀況。正確理解變量提高有助於咱們寫更好的代碼。整個變量提高的狀況總結以下:

  • var:存在變量提高,在編譯階段會被初始化爲undefined
  • let: 存在變量提高,存在暫時性死區(TDZ),執行階段,若是沒賦值,則初始化爲undefined
  • const: 存在變量提高,存在暫時性死區(TDZ),若是沒有賦值,編譯階段就會報錯;
  • function:存在變量提高,在變量聲明以前能夠訪問並執行;
  • class: 存在變量提高,存在暫時性死區(TDZ);

能力有限,水平通常,歡迎勘誤,不勝感激。

訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

前端進階學習

相關文章
相關標籤/搜索