hoisting(提升)


Posted by ericcch24 on 2020-10-16

hoisting 只會提升宣告而非賦值

console.log(b)
var b = 10
// 輸出 undefined,而非 error
-----// 上面這段可以解讀成下面
var b // 宣告變數
console.log(b)
b = 10 // 賦值

上下半部是相同意思,因為在==宣告變數的部分會被提升 hoisting==,而賦值不會,所以在經過程式碼由上而下執行之後,就不會有b is undefined的錯誤訊息,只是沒有賦值。


較常見的例子是 function 的使用,可以在 function 宣告之前就先用這個 function,再放置的順序上可以隨意。

test()

function test() {
  console.log(123)
}

-----// 上面這段可以解讀成下面

function test() {
  console.log(123)
}

test()

上下兩段是相同的,可以解讀成==整個 function 都被提升到最上面==,所以可以順利呼叫


但是當 function 是以變數宣告時

test()

var test = function() {
  console.log(123)
} // test is not a function

----// 上面這段可以解讀成下面

var test  // 只有 var 會提升到最上面

test()

test = function() { // 賦值會留在最下面
  console.log(123)
}

上下兩段一樣意思,var test = function() { console.log(123) }這段會被拆成宣告變數與賦值兩部分,因為只有 var 會提升到最上面,所以呼叫test() 會是錯誤訊息


function 宣告、function 的參數以及一般變數宣告同時出現時的提升優先順序

  1. function
  2. arguments 參數
  3. var
    註:如果宣告兩個同名的 function 或變數,後面的會取代前面的。
function test(v){
  console.log(v)
  var v = 3
}
test(10)

----

function test(v){
  var v = 10 // 因為下面呼叫 test(10)
  var v
  console.log(v)
  v = 3
}
test(10)

以上輸出是 10,參數會比一般變數宣告優先提升

console.log(a) //[Function: a]
var a
function a(){}

除了變數宣告以外,function 的宣告也會提升而且優先權比較高,因此上面的程式碼會輸出 function 而不是 undefined


小測驗

var a = 1;
function test(){
  console.log('1.', a); // undifined,因為 var a 提升 
  var a = 7;
  console.log('2.', a); // 7
  a++;
  var a;
  inner();
  console.log('4.', a); // 30
  function inner(){
    console.log('3.', a)// 8
    // 在 inner 沒有 a,
    // 往上層找到 a 是 8
    a = 30;
    b = 200; // 往上層找都沒有 b, 就變成全域變數
  }
}
test();
console.log('5.', a); // 1
a = 70;
console.log('6.', a); // 70
console.log('7.', b); // 200

流程解釋影片


Temporal dead zone:let 與 const 的 hoisting 行為

let a = 10
function test() { 
  console.log(a) // undefined
  let a = 30
}
test()

----

let a = 10
function test() { 
  let a // 跟 var 一樣會提升
  console.log(a) // error undefined
  a = 30
}
test()
  • 在變數被賦值之前,中間過程都不能存取變數,不然會發生錯誤。==而進入函式(提升之後)到賦值的這段期間叫 temporal dead zone==,這段區間內都不能存取變數的值。
  • let 與 const 也有 hoisting 但沒有初始化為 undefined,而且在賦值之前試圖取值會發生錯誤。

hositing 運作原理

Execution Contexts

  • 以下用 ES3 的規則為例

每當你進入一個 function 的時候,就會產生一個 EC,裡面儲存跟這個 function 有關的一些資訊,並且把這個 EC 放到 stack 裡面,當 function 執行完以後,就會把 EC 給 pop 出來。

簡而言之,所有 function 需要的資訊都會存在 EC,也就是執行環境裡面,你要什麼都去那邊拿就對了。

示意圖大概就像這樣,要記得除了 function 有 EC 以外,還有一個 global EC:


每個 EC 都會有相對應的 variable object(以下簡稱 VO),在裡面宣告的變數跟函式都會被加進 VO 裡面,如果是 function,那參數也會被加到 VO 裡。可以把 VO 想像成就是一個 JavaScript 的物件就好。

而 VO 在存取值的時候會用到,例如說 var a = 10 這一句,之前有講過可以分成左右兩塊:

  1. var a:去 VO 裡面新增一個屬性叫做 a(如果沒有 a 這個屬性的話)並初始化成 undefined
  2. a = 10:先在 VO 裡面找到叫做 a 的屬性,找到之後設定為 10

這邊如果 VO 裡面找不到怎麼辦?它會透過 scope chain 不斷往上尋找,如果每一層都找不到就會拋出錯誤。


當我們在進入一個 EC 的時候(你可以把它想成就是在執行 function 後,但還沒開始跑 function 內部的程式碼以前),會按照順序做以下三件事:

  1. 把參數放到 VO 裡面並設定好值,傳什麼進來就是什麼,沒有值的設成 undefined
  2. 把 function 宣告放到 VO 裡,如果已經有同名的就覆蓋掉
  3. 把變數宣告放到 VO 裡,如果已經有同名的則忽略

範例:

function test(v){
  console.log(v)
  var v = 3
}
test(10)

每個 function 你都可以想成其實執行有兩個階段,第一個階段是進入 EC,第二個階段才是真的一行行執行程式。

在進入 EC 的時候開始建立 VO,因為有傳參數進去,所以先把 v 放到 VO 並且值設定為 10,再來對於裡面的變數宣告,VO 裡面已經有 v 這個屬性了,所以忽略不管,因此 VO 就長這樣子:

{
  v: 10
}

進入 EC 接著建立完 VO 以後,才開始一行行執行,這也是為什麼你在第二行時會印出 10 的緣故,因為在那個時間點 VO 裡面的 v 的確就是 10 沒錯。

如果你把程式碼換成這樣:

function test(v){
  console.log(v) // 10
  var v = 3
  console.log(v) // 3
}
test(10)

那第二個印出的 log 就會是 3,因為執行完第三行以後, VO 裡面的值被換成 3 了。


有關 hositing 運作原理與 JS 引擎怎麼運作的詳細介紹,詳見參考資料:我知道你懂 hoisting,可是你了解到多深?

tags: Week16

#week16







Related Posts

該來理解 JavaScript 的原型鍊了

該來理解 JavaScript 的原型鍊了

[ 筆記 ] Express 03 - ORM & Sequelize

[ 筆記 ] Express 03 - ORM & Sequelize

OOP 13 - Dependency Inversion Principle

OOP 13 - Dependency Inversion Principle


Comments