溫故而知新 - 從新認識JavaScript的Hoisting

爲JavaScript裏面的概念,溫故而知新。javascript

概念:Hositing是什麼

在JavaScript中,若是試圖使用還沒有聲明的變量,會出現ReferenceError錯誤。畢竟變量都沒有聲明,JavaScript也就找不到這變量。加上變量的聲明,可正常運行:java

console.log(a);
// Uncaught ReferenceError: a is not defined
var a;
console.log(a); // undefined

考慮下若是是這樣書寫:git

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

直覺上,程序是自上向下逐行執行的。使用還沒有聲明的變量a,按理應該出現ReferenceError錯誤,而實際上卻輸出了undefined。這種現象,就是Hoistingvar a因爲某種緣由被"移動"到最上面了。能夠理解爲以下形式:github

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

須要注意函數

  1. 實際上聲明在代碼裏的位置是不會變的。
  2. hoisting只是針對聲明,賦值並不會。this

    console.log(a); // undefined
    var a = 2015;
    
    // 理解爲以下形式
    var a;
    console.log(a); // undefined
    a = 2015;

    這裏var a = 2015理解上可分紅兩個步驟:var aa = 2015prototype

  3. 函數表達式不會hoisting設計

    fn(); // TypeError: fn is not a function
    var fn = function () {}
    
    // 理解爲以下形式
    var fn;
    fn();
    fn = function () {};

    這裏fn()undefined值進行函數調用致使非法操做,所以拋出TypeError錯誤。code

函數聲明和變量聲明,都會hoisting,須要注意的是,函數會優先hoistingblog

console.log(fn);
var fn;
function fn() {}

// 理解爲以下形式
function fn() {}
var fn; // 重複聲明,會被忽略
console.log(fn);

對於有參數的函數:

fn(2016);

function fn(a) {
    console.log(a); // 2016
    var a = 2015;
}

// 理解爲以下形式
function fn(a) {
    var a = 2016; // 這裏對應傳參,值爲函數調用時候傳進來的值
    var a; // 重複聲明,會被忽略
    console.log(a);
    a = 2015;
}
fn(2016);

總結一下,能夠理解Hoisting是處理全部聲明的過程。須要注意賦值及函數表達式不會hoisting

意義:爲何須要Hoisting

能夠處理函數互相調用的場景:

function fn1(n) {
    if (n > 0) fn2(n);
}

function fn2(n) {
    console.log(n);
    fn1(n - 1);
}

fn1(6);

按逐行執行的觀念來看,必然存在前後順序,像fn1fn2之間的相互調用,若是沒有hoisting的話,是沒法正常運行的。

規範:Hoisting的運行規則

具體能夠參考規範ECMAScript 2019 Language Specification。與Hoisting相關的,是在8.3 Execution Contexts

一篇很不錯的文章參考Understanding Execution Context and Execution Stack in Javascript

參考裏面的例子:

var a = 20;
var b = 40;
let c = 60;

function foo(d, e) {
    var f = 80;
    
    return d + e + f;
}

c = foo(a, b);

建立的Execution Context像這樣:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      c: < uninitialized >,
      foo: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: undefined,
      b: undefined,
    }
    outer: <null>, 
    ThisBinding: <Global Object>
  }
}

在運行階段,變量賦值已經完成。所以GlobalExectionContext在執行階段看起來就像是這樣的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      c: 60,
      foo: < func >,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 40,
    }
    outer: <null>, 
    ThisBinding: <Global Object>
  }

當遇到函數foo(a, b)的調用時,新的FunctionExectionContext被建立並執行函數中的代碼。在建立階段像這樣:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 40, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      f: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  }
}

執行完後,看起來像這樣:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 40, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      f: 80
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  }
}

在函數執行完成之後,返回值會被存儲在c裏。所以GlobalExectionContext更新。在這以後,代碼執行完成,程序運行終止。

細節:var、let、const在hoisting上的差別

回顧規範:Hoisting的運行規則,能夠注意到在建立階段,無論是用letconstvar,都會進行hoisting。而差異在於:使用letconst進行聲明的時候,設置爲uninitialized(未初始化狀態),而var會設置爲undefined。因此在letconst聲明的變量以前訪問時,會拋出ReferenceError: Cannot access 'c' before initialization錯誤。對應的名詞爲Temporal Dead Zone(暫時性死區)。

function demo1() {
    console.log(c); // c 的 TDZ 開始
    let c = 10; // c 的 TDZ 結束
}
demo1();

function demo2() {
    console.log('begin'); // c 的 TDZ 開始
    let c; // c 的 TDZ 結束
    console.log(c);
    c = 10;
    console.log(c);
}
demo2();

總結

果然是溫故而知新,發現本身懂得其實好少。鞭策本身,後續對thisprototypeclosuresscope等,進行溫故。

參考資料

  1. 我知道你懂 hoisting,但是你瞭解到多深?
  2. MDN: Hoisting
  3. You-Dont-Know-JS 2nd-ed
  4. ECMAScript 2019 Language Specification
  5. Understanding Execution Context and Execution Stack in Javascript
  6. JavaScript高級程序設計
相關文章
相關標籤/搜索