爲JavaScript裏面的概念,溫故而知新。javascript
在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
。這種現象,就是Hoisting。var a
因爲某種緣由被"移動"到最上面了。能夠理解爲以下形式:github
var a; console.log(a); // undefined
須要注意:函數
hoisting只是針對聲明,賦值並不會。this
console.log(a); // undefined var a = 2015; // 理解爲以下形式 var a; console.log(a); // undefined a = 2015;
這裏var a = 2015
理解上可分紅兩個步驟:var a
和a = 2015
。prototype
函數表達式不會hoisting
。設計
fn(); // TypeError: fn is not a function var fn = function () {} // 理解爲以下形式 var fn; fn(); fn = function () {};
這裏fn()
對undefined
值進行函數調用致使非法操做,所以拋出TypeError
錯誤。code
函數聲明和變量聲明,都會hoisting
,須要注意的是,函數會優先hoisting
:blog
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。
能夠處理函數互相調用的場景:
function fn1(n) { if (n > 0) fn2(n); } function fn2(n) { console.log(n); fn1(n - 1); } fn1(6);
按逐行執行的觀念來看,必然存在前後順序,像fn1
與fn2
之間的相互調用,若是沒有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
更新。在這以後,代碼執行完成,程序運行終止。
回顧規範:Hoisting的運行規則
,能夠注意到在建立階段,無論是用let
、const
或var
,都會進行hoisting
。而差異在於:使用let
和const
進行聲明的時候,設置爲uninitialized
(未初始化狀態),而var
會設置爲undefined
。因此在let
或const
聲明的變量以前訪問時,會拋出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();
果然是溫故而知新,發現本身懂得其實好少。鞭策本身,後續對this
、prototype
、closures
及scope
等,進行溫故。