從 Hoisting 理解底層運作機制


Posted by saffran on 2021-02-25

好文推薦:我知道你懂 hoisting,可是你了解到多深?

什麼是 hoisting(提升)?

下面這樣寫,程式會出錯

console.log(b); // b is not defined

但是,改成下面這樣後,程式就可以正常執行了

console.log(b); // undefined
var b = 10;

上面的程式碼,可以想成是這樣:把「變數的宣告」提升到第一行

變數的宣告會 hoisting

注意,只有「宣告」會 hoisting,「賦值」不會 hoisting

var b;
console.log(b);
b = 10;

function 的宣告也會 hoisting

test();

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

上面的程式碼,可以想成是這樣:把「function 的宣告」提升到第一行

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

test();

hoisting 需注意的地方

注意,下面這樣寫,程式碼會出現錯誤:test is not a function

test();

var test = function () {
  console.log(123);
};

原因為:
只有「變數的宣告」會 hoisting,「變數的賦值」不會 hoisting

所以,上面的程式碼可以看成是這樣:
在呼叫 test() 時,test 是 undefined,並不是一個 function,所以會出錯(test is not a function)

var test; // 變數的宣告

test(); // 這時的 test 是 undefined

test = function () { // 變數的賦值
  console.log(123);
};

hoisting 只會發生在「變數自己的 scope 裡面」

因為 hoisting 是跟變數有關的,所以 hoisting 只會發生在「變數自己的 scope 裡面」

舉例:

var a = "global";

function test() {
  console.log(a); // undefined
  var a = "local";
}

test();

console.log(a) 會印出 undefined 是因為:
在 test scope 裡面,var a = "local" 這行會有 hoisting,可以看成是這樣:

var a = "global";

function test() {
  var a; // 「變數的宣告」會 hoisting
  console.log(a); // undefined
  a = "local"; // 「變數的賦值」不會 hoisting
}

test();

hoisting 的順序:function > arguments > var

在 hoisting 時,優先權是 function > 變數

同時宣告了一個 function a 和變數 a,這時,function 會有 hoisting 的優先權

function test() {
  console.log(a); // [Function: a]
  function a() {}
  var a = "local";
}

test();

上面這段程式碼可以看成是這樣:

function test() {
  function a() {} // function 會有 hoisting 的優先權
  console.log(a); // [Function: a]

  a = "local";
}

test();

同時宣告兩個同名的 function 時,後者會覆蓋掉前者

function test() {
  console.log(a); // [Function: a]
  a(); // 2

  function a() {
    console.log(1);
  }
  function a() {
    console.log(2);
  }
  var a = "local";
}

test();

參數的 hoisting 順序

「參數」會優先於「宣告變數」

下面的 console.log(a) 會是 123,不會是 undefined

function test(a) {
  console.log(a); // 123
  var a = 456;
}

test(123);

就算是把上面的程式碼看成這樣:
就算 var a = 456 有 hoisting,但是如果 a 沒有重新賦值,a 的值就會是我傳進去的參數 123

下面的 var a 這句的意思只是「我要宣告一個變數 a」,但是因為在 test scope 裡面已經存在變數 a 了,所以 var a 這句就會被忽略(並不會把 a 的值變成 undefined)

function test(a) {
  var a;
  console.log(a); // 123
  a = 456;
  console.log(a); // 456
}

test(123);

再舉另一個例子:
var a 這句會被忽略,因為在 test scope 本來就已經存在變數 a

function test(a) {
  var a = "test";
  console.log(a); // test
  var a; // 我要宣告一個變數 a (會被忽略,因為本來就已經有變數 a 了)
}

test();

「function」會優先於「參數」

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

test(123);

hoisting 的原理為何?從 ECMAScript 下手

參考文章 我知道你懂 hoisting,可是你了解到多深?

Execution Contexts(簡稱 EC)就是「執行環境」

作用域和 hoisting 的小測驗

請問 log 出來的值依序會是什麼?

var a = 1;
function test() {
  console.log("1.", a); // undefined
  var a = 7;
  console.log("2.", a); // 7
  a++; // a = 8
  var a; // 忽略
  inner();
  console.log("4.", a); // 30
  function inner() {
    console.log("3.", a); // 8
    a = 30; // 這個 a 是「存活在 test scope 的區域變數」
    b = 200; // b 變成全域變數了
  }
}
test();
console.log("5.", a); // 1 (這個 a 是全域變數的 a)
a = 70;
console.log("6.", a); // 70
console.log("7.", b); // 200

output:

1. undefined
2. 7
3. 8
4. 30
5. 1
6. 70
7. 200

let 與 const 的詭異行為

如果是用 var 宣告變數

因為 hoisting,console.log(a) 會是 undefined

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

如果是用 letconst 宣告變數

console.log(a) 都會出錯,顯示「ReferenceError: Cannot access 'a' before initialization」

console.log(a);
let a = 10;
console.log(a);
const a = 10;

letconst 是有 hoisting 的

因為如果沒有 hoisting 的話,那在 function test 裡面的 a,應該會往上層找到 let a = 10,但是 console.log(a) 的結果並不是 10,是出現了錯誤「ReferenceError: Cannot access ‘a’ before initialization」

所以 letconst 是有 hoisting 的,只是 hoisting 的方式跟 var 不同(底層的運作方式不同)

let a = 10;
function test() {
  console.log(a); // Cannot access 'a' before initialization
  let a = 30;
}

test();

letconst 的 hoisting 運作方式,請接著往下看

TDZ:Temporal Dead Zone

letconst 的 hoisting 運作方式:不能在「賦值」之前,去存取變數

上面那段程式碼,的確是可以看成下面這樣(hoisting):
如果第三行是寫 var a;a 會被初始化成 undefined,所以 console.log(a) 就會是 undefined

但是,如果是 let a; 或是 const a,變數都不會被初始化成 undefined

而且,直到「執行到第五行 a = 30; 之前」,我都不能去存取 a

從「進入 function」(hoisting)開始,一直到「賦值給變數」之前的這段區間,就是這個變數的 Temporal Dead Zone

以下面範例來說,Temporal Dead Zone 就是從「第三行開始到第五行」之間

  • 在 Temporal Dead Zone 裡面,都不能去存取變數的值。如果存取的話,就會出現錯誤
let a = 10;
function test() {
  let a;
  console.log(a);
  a = 30;
}

test();

#javascript







Related Posts

MTR04_0728

MTR04_0728

MTR04_0613

MTR04_0613

Vue.js 學習旅程Mile 3 – Vue 實例 與 MVVM 篇

Vue.js 學習旅程Mile 3 – Vue 實例 與 MVVM 篇


Comments