以最膚淺的說法,一句話說明hoisting的行為表現就是:在宣告前就能使用函式。像下面的例子。但實際上這個東西神祕到我不懂它為什麼會存在。
console.log(calcCarAgeDec(2018)); //5
function calcCarAgeDec(buyYear) {
return 2023 - buyYear;
}
據我的理解,這個東西存在的原因可以分成兩個方面來說,一個可以從效能來討論,另一個則是是從歷史的角度切入。
外星語版本
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天快速開發,這樣的特性對於當時的需求應該是很實際而且適合,只是沒想到後來的發展超展開,變成操控網頁行為的主流程式語言,這個當初為了"方便"而作的小把戲,好像變成了一個讓人摸不清頭緒的存在。
以下是用一種不百分百精確但簡單易懂的方式來做歸納,流程是這樣:
以下就是按流程去看各種函式與變數(其實會有點重複)
console.log(a); //undefined
var a = 3;
console.log(b);
const b = 5;
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行為,最讓人困惑的是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”.