iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
Modern Web

超低腦容量學習法遇到javascript系列 第 4

Hoisting它到底是不是個好東西?

  • 分享至 

  • xImage
  •  

以最膚淺的說法,一句話說明hoisting的行為表現就是:在宣告前就能使用函式。像下面的例子。但實際上這個東西神祕到我不懂它為什麼會存在。

console.log(calcCarAgeDec(2018)); //5

function calcCarAgeDec(buyYear) {
  return 2023 - buyYear;
}

為什麼要 hoisting?

據我的理解,這個東西存在的原因可以分成兩個方面來說,一個可以從效能來討論,另一個則是是從歷史的角度切入。

先從hoisting的機制來討論效能

外星語版本
js執行前,會先掃過整份程式碼,在每一個execution context,依變數或函式名稱在variable environment object新增一個屬性,接著再以直譯器(Interpreter)的方式,逐行轉成machine code,如果執行的過程中遇到了函式或變數,就可以直接從在物件中找到,而不用再從整份程式碼中尋找當初宣告的內容,嗯..聽起來是的確有效率地多(大概吧)

翻譯蒟篛
js執行前,會先掃過整份程式碼,先記得有哪些變數和函式,接著再逐行執行,這時候,如果執行的過程中遇到了函式或變數,就不用再重新尋找,所以會比較快。

參考資料:

歷史共業

直接引用參考資料JavaScript Hoisting: What It Is And Why It Was Implemented挖到的推特文,由js作者回覆的

@aravind030792
function hoisting allows top-down program decomposition, 'let rec' for free, call before declare; var hoisting tagged along.

@aravind030792 var hoisting was thus unintended consequence of function hoisting, no block scope, JS as a 1995 rush job. ES6 'let' may help.
— BrendanEich (@BrendanEich) October 15, 2014

看起來是當初以top-down的思維設計這套程式時,為了方便把大問題切割成小問題而採用的作法(類似遞迴的概念),畢竟只花10天快速開發,這樣的特性對於當時的需求應該是很實際而且適合,只是沒想到後來的發展超展開,變成操控網頁行為的主流程式語言,這個當初為了"方便"而作的小把戲,好像變成了一個讓人摸不清頭緒的存在。

各種hoisting行為

以下是用一種不百分百精確但簡單易懂的方式來做歸納,流程是這樣:

  1. 先看js執行前的掃描做了什麼
  2. 在執行程式時,遇到未宣告的變數或函式會到variable environment object那個物件去找,找到的東西就是hoisting行為

以下就是按流程去看各種函式與變數(其實會有點重複)

  • function declaration:
    1. 當掃到function declaration,在variable object以函式名稱新增一個key,其value為宣告的內容。
    2. 找到function內容,a.k.a可在宣告前就執行
      (例子放在下面與function expression & arrow function 一起看)
  • var 變數:
    1. 在variable object以var變數名稱新增一個key,其value為undefined
    2. 找到undefined,a.k.a在宣告前想存取,會得到undefined
console.log(a); //undefined
var a = 3;
  • let & const變數:
    1. 在variable object以let(or const)名稱新增一個key,其value為uninitialized
    2. 在物件中有找到這個屬性,但為未初始化狀態,所以報錯
console.log(b);
const b = 5;

  • function expression & arrow function:看是存在哪種變數,其表現如同那種變數(見上方var, let/const結果)
    以下比較function declaration & 各種變數的function expression
console.log(calcCarAgeDec(2018)); //5

console.log(calcCarAgeExp1); //undefined
console.log(calcCarAgeExp1(2018)); //報錯:caught TypeError: calcCarAgeExp1 is not a function
// undefined is not a function

console.log(calcCarAgeExp2(2018));
//報錯:caught ReferenceError: Cannot access 'calcCarAgeExp2' before initialization
// 位於TDZ,尚未初始化

function calcCarAgeDec(buyYear) {
  return 2023 - buyYear;
}

var calcCarAgeExp1 = function (buyYear) {
  return 2023 - buyYear;
};

const calcCarAgeExp2 = function (buyYear) {
  return 2023 - buyYear;
};

其實想補充一個就是,有看到一個說法:宣告只有變數提升,賦值留在原地,我個人是不會想要這樣看事情,這是一個現象,就是在pre-scan的時候,本來看到var的宣告,不管你賦值了沒,js就只是先給一個undefined在物件的value,等執行階段讀到var宣告那行才會做到賦值,把它當成一個原則只會讓我感覺到混亂,供參考。
至於hoisting的順序,我也不是很想知道(這態度對嗎),畢竟,我自己是覺得在寫code的時候,非必要我不會想要用hoisting,所以不想浪費自己的記憶體在理解這個我覺得我大概不會遇到的東西,況且,遇到了,在測試看看就知道結果。

hoisting你壞壞

以上三種hoisting行為,最讓人困惑的是var,它既不會報錯,但也無法像function declaration那樣抓到實際宣告的內容,甚至js作者也說,var hoisting是function hoisting的"非預期後果",下面用個簡單例子說明,hoisting可能會造成難以察覺的bug:
如果所選擇的產品數量為0,則刪除訂單;但由結果可以發現,即使產品數量為10,在var hoisting的作用下,!productCount(也就是!undefined)仍為true,函式deleteOrder會執行。

if (!productCount) deleteOrder();

var productCount = 10;
function deleteOrder() {
  console.log("Your order has been deleted!");
}

ES6引進了let, const兩種block scope的變數,改善了變數hoisting的令人迷惑的特性,同時引進了暫時死區(Temperery dead zone, TDZ)的概念。
TDZ的範圍是從當前scope的第一行,到const(or let)宣告的那一行,js即使知道這個變數在程式碼的某處有被宣告,但在這個範圍內就是暫時無法使用,如果在TDZ內試著存取就會報錯,其目的就是要更容易做到避免及揪出錯誤。

今日總結

總之,以目前我小嫩嫩的程度,是盡量不會用hoisting啦,先宣告再呼叫,只用const & let,這樣。
最後,hoisting真的一無是處嗎?我想當然不是的,在好幾篇參考資料都有看到,為了達成mutual recursion,function hoisting是有必要的,但我想今天的quota應該已經夠了,這個話題就先停在這邊,如果後面找不到題目了,我們再考慮來看看這個XD
Note 4. Two words about “hoisting”.


上一篇
兩種函式宣告的方法:function declaration & function expression (看起來都一樣阿~~)
下一篇
各種迴圈for/while/do while的使用時機
系列文
超低腦容量學習法遇到javascript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言