JS 為什麼要有 hoisting

MoreCoke
12 min readApr 27, 2020

--

我寫文前對 hoisting 的理解只有:

  1. 宣告只有變數提升,賦值留在原地
  2. 函式優先,引數第二,變數最後
  3. let 和 const 不會提升 ( 這是錯的,後面會解釋 )

平常寫程式也習慣把變數宣告在最上方,所以也沒遇到什麼問題,就這樣一直寫到現在。

但最近求職被問到 hoisting 的相關問題,雖然有答出來,但發現自己只懂表面而已,我知道怎麼做,但不知道背後的原理,所以這篇文出現啦~

hoisting 是什麼?

細思極恐,我一直認為 JS 是直譯式語言,對於 hoisting 只當做是 JS 的特色沒多想,可是如果真的是逐行編譯的話,怎麼會有函式提升這種事情,執行第一行的程式碼能呼叫第二行的 function 拿來使用?

sayhi();   // 一行一行執行會報錯吧?
function sayhi() {
console.log('hi');
};

我對編譯類型的理解

電腦只看得懂 0 和 1 ( 也就是 machine code ),所以要讓電腦執行我們寫的 code 時需要透過編譯器 ( Compiler ) 或直譯器 ( Interpreter ) 幫忙翻譯我們寫的 code 讓電腦看懂。

直譯式特色

  • 把你寫的 code 透過直譯器逐行編譯成 machine code 和執行該行程式碼,出現錯誤立即中斷編譯
  • 除錯容易,執行速度慢
  • 語言 : JS

編譯式特色

  • 先把你全部的 code 透過編譯器轉成 machine code 再執行
  • 除錯困難,執行速度快
  • 語言 : C

其他( 混合式?我亂取的 )

  • 先把你的 code 全部編譯成 byte code,再透過 Java 的模擬器 JVM 直譯成 machine code 逐行執行
  • 語言 : Java ( 不過看到滿多人歸類在直譯式 )

關於這部分想知道更多可看

好,拉回主線,我回去翻你所不知道的 JS 這本書,看了關於 hoisting 這個章節,其中有句話挺妙的,引擎會在你的 JS 程式碼直譯前先編譯它(在中文書範疇系列的 48 頁,章節編譯器再次出擊的第一段第二行中後段)。

什麼跟甚麼 ???這跟上面說的混合型 ( Java )好像有點像,可是還是看不懂,後來看了這兩篇文有了頭緒 :

引用 “ 我知道你懂 hoisting,可是你了解到多深? “ 文中的一段話

事實上很多種直譯器內部的運作方式都是先把原始碼編譯成某種中間碼再去執行,所以編譯這個步驟還是很常見的,而 JS 也是這樣運作的。

簡單來說, JS 的引擎在執行我們寫的 code 前,會先將我們的 code編譯成引擎自己比較方便看懂的程式碼後再逐行執行。

hoisting 的優先順序怎麼知道的?

我忘記是怎麼知道的了,而且平常命名變數也不可能會取一樣的名字搞自己,所以一起試出答案吧!

function sayhi(text) {
console.log(text);
var text = 123;
function text() {
return '我是 function';
}
}
sayhi('你好阿');

這邊會得到 function ,知道 function 優先後,剩下 argument 和 variable 的比較了。

刪除 function 來看剩下兩個的關係,下面會印出 undefined 還是 ‘你好阿’ ?

function sayhi(text) {
console.log(text);
var text = 123;
}
sayhi('你好阿');

答案是 ‘你好阿’ ,因為 argument 比 variable 宣告要來的早,所以會出現‘你好阿’ ,如果改成下方這樣,那又會印出 ‘123’ 了。執行流程是 text 原本是 ‘你好阿’ ,然後執行第一行 var text = 123; , text被改成 123 ,所以結果是 123。

function sayhi(text) {
var text = 123;
console.log(text);
}
sayhi('你好阿');

這樣我們就知道優先順序了~

  1. 函式
  2. 引數
  3. 變數

那改順序呢? console 放在最後?

function sayhi(text) {
function text() {
return '我是 function';
}
var text = 123;
console.log(text);
}
sayhi('你好阿');

我不說答案啦,每個順序打亂自己試試看。

那同名 function 的優先順序呢?

function sayhi(text) {
console.log(text);
function text() {
return '吃飽';
}
function text() {
return '睡飽';
}
}
sayhi('我都要');

會發現是後來命名的 function 會覆蓋前一個相同名稱的 function

如果我想逐行執行程式碼,不希望發生 hoisting 要怎麼寫比較好?

我越想越覺得怪,為甚麼會有不希望發生 hoisting 的情況發生呢? 那 hoisting 的出現是為了解決甚麼問題?

我看到了這篇文章 : Two word about “hoisting”. ,學到可以使用 function expression 的方式避免 hoisting。

關於 function delcaration 和 function expression 的範例

function expression 的寫法

var foo = function () {
console.log('hello');
};foo();

function delcaration 的寫法

foo();function foo(){
console.log('hello');
}

不希望發生 hoisting 的情況

自己寫程式會避免命名相同變數,但在大型專案多人合作的時候呢?用 function expression 就是解決方法之一啦。

假如剛好有兩個人命名相同的 function,並在最後合併 js 檔案的時候會出現什麼事? 假設現在有 a.js 和 b.js ,並用 gulp 來合併專案:

// a.js
function
done(){
console.log('A done!');
}
done();
// b.js
function
done(){
console.log('B done!');
}
done();

用 gulp 合併,執行 gulp execute 自訂指令後,會將 a.js 和 b.js 合併成 all.js,再輸出到 build 的資料夾,附上資料夾結構和程式碼。

var gulp = require('gulp');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
gulp.task('execute',function(){
var stream = gulp.src('src/*.js')
.pipe(concat('all.js'))
.pipe(uglify())
.pipe(gulp.dest('build'));
return stream;
})

接著來看 all.js

function done(){
console.log("A done!");
}
function done(){
console.log("B done!");
}
done(), done();

其中 a.js 和 b.js 如果有相同名稱的 function 會因為 hoisting 被覆寫,不知不覺的在專案中埋了顆地雷。

那 hoisting 的出現是為了解決甚麼問題?

我也不知道,原本我是 google 說 why we need hoisting in javascript 找到這篇討論 : Why does JavaScript hoist variables? ,內容講的是 JS 在引擎如何編譯,看了其他文章也講差不多的內容,後來還是回到這篇文章 : 我知道你懂 hoisting,可是你了解到多深?

裡面提到說 hoisting 可以讓函式互 call ,這種現象也叫交互遞迴,

關於遞迴可以看這篇文章 : Good Morning, Functional JS (Day 25, Recursion 遞迴)

照樣照句,寫個簡單的範例 :

function aaa(n) {
if(n > 1) bbb(n-1);
return;
}
function bbb(n) {
console.log('倒數',n)
if( n == 1) return;
aaa(n);
}
aaa(5);
if( n == 1) return;
aaa(n)

變數的生命週期

這裡用 var 變數來解釋什麼是變數的生命週期,關於下方的變數,實際執行時 JS 在背後做了什麼?

var favoriteDrink = ‘Coke’

生命週期共分為三個階段

  1. 宣告 ( Declaration )
  2. 初始化 ( Initialization )
  3. 指派 ( Assignment )

畫圖表示會比較清楚

在我們命名提升 var favoriteDrink 時,JS 會提升宣告,並初始化為 undefined, 等到 JS 編譯到 var favoriteDrink = ‘Coke’,才會指派值。

函式的生命週期

函式的更簡單,宣告、初始化和指派全部提升一次到位!

function sayHi() {
console.log('Hi);
}

let 和 const 其實有提升

但不同的是當它們宣告提升時不是給 undefined 而是 uninitialization ,因為這樣的差別,導致 let 和 const 的宣告在尚未賦值前存取會出錯

let 的版本

function sayHi() {
//提升 let message; // uninitialization
console.log(message) //出錯
let message = ‘hello!’; // initialization 和 create binding with ‘hello’
console.log(message)
}

var 的版本

function sayHi() {
//提升 var message // initialization with undefined or function
console.log(message) // uninitialization
var message = 'hello!'; // create binding with 'hello'
console.log(message)
}

而 let 的存取錯誤提示也不同以往,會是 Cannot access ‘message’ before initialization ,意思是在變數初始化以前不能存取!

Uncaught ReferenceError: Cannot access 'message' before initialization

而不是變數尚未定義

Uncaught ReferenceError: message is not defined

let 和 const 這種在變數賦值前不能存取的現象稱為 Temporal Dead Zone。

function sayHi() {
//提升 let message; TDZ 開始
console.log(message) //出錯
let message = ‘hello!’; // TDZ 結束
console.log(message)
}

var 跟 function 都畫圖了,就順便畫 let 的。

let favoriteDrink = ‘Coke’

參考資料 : JavaScript Variables Lifecycle: Why let Is Not Hoisted

後記

下方的連結文章寫得很完整,真的能去看看,我在寫文時一直在揣測當初寫這篇文章的 Huli 大大遇到問題時是怎麼找資料的,所以非必要我都會先自己查過其他資料,如果真的不懂再來看他文章中的參考資料,再看不懂最後才會看 Huli 大大寫的內容,我覺得學習如何找對的資料很重要。

原本只是想記一下面試學到的東西,開始寫文後發現寫這種文章超累的,明明網上就有現成的好文章,只要爬文就能解決當下的問題,幹嘛還花時間寫筆記? 每次文章寫完一個段落後,腦中都會浮現這個問題。不過累歸累,目前還是覺得這樣是修正自己思考的最好方式,練習遇到 bug 該如何解決的思考模式。

我發現過去自己很多時候在寫這種文的時候都是用已知問題答案的前提去探討問題,這樣並不是真的搞懂,這樣只是懂表面而已,為了避免頭痛醫頭,腳痛醫腳的情況,現在回來將基本觀念複習補齊。當然文中廢話也挺多的,不過我相信之後寫的文章會愈來越好的。

下次見,掰掰。

--

--

MoreCoke

努力補技術債。和我一同攻略前端技術吧。