JavaScript Scope

變量作用域

JavaScript 有兩種變量作用域:

  • Gloable Scope: 定義在函數之外的為全局變量,整個程序都可以訪問,直到頁面關閉才銷毀。
  • Local Scope: 定義在函數之內局部變量,函數執行開始到結束,會創建與銷毀該變量。

以下列出一些特性:

  • 局部變量不影響全局變量:
var str = 'hello';

function test() {
  var str = 'hi';
}

test();

str += ' R00.';

console.log(str) // print `hello R00.`
  • 沒有宣告視為全局聲明:
console.log('hello :' + name) // print `hello Yuyu`

// 此處 function 其實就是一個全局的聲明
function test() { 
    name = "Yuyu";
}

console.log('hello :' + name) // print `hello Yuyu`
  • 閉包將保存局部變量的引用:
function greet(name) {
  
  return function () {
    sayHello(name);
  }
}

function sayHello(name) {
  console.log('hello ' + name);
}

var func = greet('Yuyu');
func(); // print `hello Yuyu`

sayHello('Jay'); // print `hello Jay`

func(); // print `hello Yuyu`
  • 變數的提升 hoisting:
var number = 100;
test();

function test() {
  console.log(number); // print `undefined`

  if (false) {
    var number = 123;  // never call
  }
}

關於提升 hoisting 可以簡單的理解為提前訪問

區塊作用域

區塊作用域 Block Scope 在很多語言都有,但是 JavaScript 沒有,直到 ES6 後才導入此特性。

varlet / const 的比較:

  • var 為函數作用域
  • let / const 為 區塊作用域

幾個經典的作用域例子:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
let i = 0
for (; i < 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
  • var 在聲明之前被訪問會拋出 undefined
  • let / const 在聲明之前被訪問會拋出 ReferenceError
console.log(_var) // undefined
console.log(_let) // ReferenceError: _let is not defined
var _var = 1
let _let = 2

根據 ES6 標準規範 13.3.1

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.

A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

簡單是說由 let / const 聲明的變量,只有在詞法綁定 (LexicalBinding) 給值後,才能夠被訪問。 變量的初始化必須經過賦值,如果 let 在綁定過程沒給值,將以 undefined 當作初始值。

  • 進一步我們可以觀察到 var 與 let 在聲明都有被 hoisting,都會先在作用域上被創建出來,但是 let 在詞法綁定之前被訪問的話會拋出錯誤。
  • 變量被創建變量可以被訪問(初始化完成)的這一段時間稱之為 Temporal Dead Zone (TDZ)

提升 hoisting 是 JS 基本的特性,在有些地方會將 let / const 歸類到無法 hoisting,但其實不然,let / const 也是會受到提升,只是因為 TDZ 作用的關係,所以拋出錯誤 ReferenceError,跟 var 拋出 undefined 有所不同。

Temporal Dead Zone (TDZ)

以下我們來看幾個 TDZ 常發生錯誤的例子:

let x = x

console.log(x)
function foo(x = y, y = 1) {
  console.log(y)
}

foo(1) // good
foo(undefined, 1) // ReferenceError: `y is not defined`
foo() // ReferenceError: `y is not defined`
function f() { return x }
f() // ReferenceError
function f() { return x }
let x = 1
f() // nothing error
let x = 1

function foo(a = 1, b = function(){ x = 2 }){
  let x = 3
  b()
  console.log(x)
}

foo()

console.log(x)

// babel Compiler: 2, 1
// Closure Compiler: 3, 2
// Google Chrome(v55): 3, 2
// Firefox(v50): 2, 1
// Edge(v38): 3, 2
function foo(a = 1, b = function(){ let x = 2 }){
  b()
  console.log(x)
}
foo()

// Chrome: ReferenceError: x is not defined
// Firefox: ReferenceError: x is not defined

雖然 ES6 制定了標準,但目前 TDZ 在作用域上各家瀏覽器實作略有不同,所以盡量避免在參數預設值上做有副作用的運算