iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 13
2
Modern Web

你懂 JavaScript 嗎?系列 第 13

你懂 JavaScript 嗎?#13 拉升(Hoisting)

你所不知道的 JS

本文主要會談到

  • 什麼是拉升(hoisting)?
  • 變數與函式的拉升有什麼不同?
  • 怎麼處理在 <script> 宣告的全域變數?是否也有拉升的狀況?
  • 拉升 vs 重複宣告的變數與函式,要怎麼處理?

什麼是拉升(Hoisting)?

你有遇過這種靈異狀況嗎?明明變數尚未被宣告,但用的時候居然沒被報錯,還得到值 undefined???咦?等等,怎麼不是得到 2???

console.log(a); // undefined
var a = 2;

...

...

當年 我真的被這個問題嚇到惹。到底發生了什麼事?(*´・д・)?

嚇到吃手手

...

...

隔壁同事都來圍觀。

到底怎麼惹?

...

...

接著,資深工程師看不下去了牽起我的手 不,是丟了一本書給我,要我回家好好讀,免得再丟人現眼。

你知道自己智商多低嗎?

「這是 hoisting 好嗎!」

...

...

好的,如果你也有這個問題,很正常你沒病,接下來要做的只是好好搞懂它,誰叫這一系列叫做「你懂 JavaScript 嗎?」

...

...

先來看編譯器怎麼看待這段程式碼。

console.log(a); // undefined
var a = 2;

一開始,編譯器在編譯時期會先找出所有的變數並綁定所屬範疇,但不賦值,所以此刻變數所帶的值是 undefined;而在執行階段,JavaScript 引擎才會處理給值的事情。

因此,上面這段程式碼,在編譯器的眼中,其實是這樣的...

var a; // 編譯時期確認 a 屬於全域範疇,但不賦值,所以此刻變數所帶的值是 undefined

console.log(a); // 得到 undefined
a = ?; // 執行時期才會知道 a 的值是什麼

console 的結果是 undefined,而非 RHS 解析失敗的「ReferenceError: a is not defined」,畢竟變數已經被定義了,只是不知道真正的值是什麼而已。

通常,我們可想像成這是因為編譯器會先掃過程式碼中的宣告的變數和函式,而把這些變數和函示「提升」到程式碼的最頂端,因此當印出 a 的值的時候,會是已宣告但還沒賦值的狀態,也就是有這個變數,但其值是 undefined。

結合編譯和執行時期的分工狀況,可想成是這樣...

var a; // 編譯時期的工作

console.log(a); // 執行時期的工作
a = 2; // 執行時期的工作

又,以下有幾點需要注意的議題...

拉升是逐範疇的!

也就是說,在函式內宣告變數,不會被拉升到全域範疇而成為全域變數。

函式運算式不會被提升

函式運算式(function expression)不會被提升。如下,foo 此時的值是 undefined,所以當執行 undefined() 就會得到 TypeError。而 bar 這個識別字是屬於 foo 的範疇的,所以在這裡會報錯 ReferenceError。

foo(); // TypeError
bar(); // ReferenceError

var foo = function bar() {
  // ...
};

實際上應該看成...

var foo;

foo(); // TypeError
bar(); // ReferenceError

var foo = function bar() {
  var bar = ...self...
  // ...
};

這是因為拉升只會有變數宣告的部份,後續的指定等運算都不會跟著提升。

變數 vs 函式的拉升

變數與函式的拉升的不同之處在於,變數的拉升只有宣告部份,而函式的拉升是整個函式,因此函式在宣告前是可以執行的。

多個 <script> 中的全域變數並不會被拉升

在不同 <script> 包圍,即視為不同的檔案,因此某支檔案中的變數宣告不會被拉升到另一支檔案的頂端。

<script>foo();</script>

<script>
  function foo() { .. }
</script>

但是呢,以下兩種情況是允許的,可正常執行。

<script>
  foo();
  function foo() { .. }
</script>

<script>
  function foo() { .. }
</script>

<script>foo();</script>

上例與拉升無關,而是因為在同一支 HTML 檔案中嵌入的 <script> 共用同一個全域範疇,因此在第一個 <script> 中宣告了 foo 函式而成為全域物件的屬性,在第二個 <script> 中就可以使用這個屬性了。

重複宣告

若函式和變數同名,則函式會優先;若同時有多個函式同名,則後面的會覆寫前面的宣告。

範例 1:若函式和變數同名,則函式會優先

範例如下,同名函式 foo 和變數 foo,由於函式優先,因此 foo() 得到 1 而非 undefined() 的結果 TypeError。

foo(); // 1

var foo;

function foo() {
  console.log(1);
}

foo = 2;

這是因為要看成...

function foo() {
  console.log(1);
}

foo(); // 1

foo = 2;

範例 2:若同時有多個函式同名,則後面的會覆寫前面的宣告

範例如下,三個同名函式 foo,最後一個 foo 會覆寫前面的宣告,因此得到 3。

foo(); // 3

function foo() {
  console.log(1);
}

function foo() {
  console.log(2);
};

function foo() {
  console.log(3);
}

...

...

其實也沒這麼困難 靈異 嘛!終於弄懂啦!

ya

...

...

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...

  • 編譯器在編譯時期會先找出所有的變數並綁定所屬範疇,但不賦值,所以此刻變數所帶的值是 undefined;而在執行階段,JavaScript 引擎才會處理給值的事情。可以想成是把這些變數和函示「提升」到程式碼的最頂端,這就是所謂的拉升(hoisting)。
  • 拉升是逐範疇的,在函式內宣告變數,不會被拉升到全域範疇而成為全域變數。
  • 函式運算式不會被提升。
  • 變數與函式的拉升的不同之處在於,變數的拉升只有宣告部份,而函式的拉升是整個函式,因此函式在宣告前是可被執行的。
  • 同一支 HTML 檔案中嵌入的多個 <script> 時,不同 <script> 包圍的即視為不同的檔案,因此某支檔案中的變數宣告不會被拉升到另一支檔案的頂端。
  • 若函式和變數同名,則函式會優先;若同時有多個函式同名,則後面的會覆寫前面的宣告。

References


同步發表於部落格


上一篇
你懂 JavaScript 嗎?#12 函式範疇與區塊範疇(Function vs Block Scope)
下一篇
你懂 JavaScript 嗎?#14 動態範疇(Dynamic Scope)
系列文
你懂 JavaScript 嗎?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言