語法速覽

本小節快速說明 JavaScript 的語法, 若曾有程式開發經驗, 只要快速略過就好

變數宣告

var i = "str"; // 宣告一個變數

let j = 0;     // 宣告一個變數, After ES6

const k = 100; // 宣告一個常數, After ES6

/*
 * 多行註解
 */

// 單行註解

在早期(ES6以前), 通常使用 var 關鍵字來宣告變數, ES6以後的版本則可以使用 letconst 宣告變數, 不過 var 以及 let 的行為並不相同, 最好都使用 let來宣告, 這個問題保留在此處說明1

Tip

絕大多數, 請優先使用 const 進行宣告, 其次才是使用 let 宣告

若要明確聲明一個全域變數, 可以用globalThis.locale = "zh-TW" 的方式來宣告, 以宣告成員的方式指定一個變數, 這可以很好的釐清變數所在的scope

型別字面值

字面值的意思是, 直接提供給 JavaScript 變數的值, 且不使用建構式初始化, 舉例來說, 陣列可以使用以下兩種方式宣告:

let users = ['Alex', 'Bob', 'Claire']; // literals

let users = new Array('Alex', 'Bob', 'Claire'); // constructor

在多數的情況下, 最好使用字面值(literals) 來初始化內建型別

  • Boolean: true, false
  • Numerical:
    • (十進制) decimal: 0, 100, -123
    • (八進制) octal: 015, 021, -0o73
    • (十六進制) hex: 0x15, 0x153, -0xf1A7(a~f 不區分大小寫)
    • (二進制) binary: 0b11, 0b0011, -0b11
  • Floating:.123, -.01, 0.123, -0.1234, 1e-34, 1E+12
  • String:'str, asd \n\r', "123 45 a1bd", `template string`
  • RegExp: /[a-z]*/
  • Array: [0,1,2,3,4,5]
  • Object: { a: 30, b: "str", c: true }

比較特殊的部分是Template String, 他是由兩個 ` 符號構成的字串, 可以在其中使用${ expr }嵌入表達式:

const name = "Alex";
const age = 18;

const info = `${name} is ${age} year-old Man`
// 輸出 "Alex is 18 year-old Man"

定義類別

class Rect {
  constructor(x1, y1, x2, y2) {
    /* 成員在建構式中宣告 */
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
  }

  /* 宣告方法 */
  getArea() {
    return Math.abs((this.x2 - this.x1) * (this.y2 - this.y1));
  }
}

class Rect {
  constructor(x1, y1, x2, y2) {
    /* 成員在建構式中宣告 */
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
  }

  /* 宣告方法 */
  getArea() {
    return Math.abs((this.x2 - this.x1) * (this.y2 - this.y1));
  }
}

/* 繼承 */
class Square extends Rect {
  constructor(x1, y1, x2, y2) {
    super(x1, y1, x2, y2); // 呼叫父類別的建構式
  }
}

陣列

let numberArr1 = [1,2,3,4,5];

let numberArr2 = new Array([1,2,3,4,5]); // 這是一個二維陣列

let charArr = Array.from("abcdef"); // 若引數是擁有迭代器介面的型別, 則可以通過 Array.from 創造一個淺層陣列
// result: charArr = ['a','b','c','d','e','f'];

function range( start, stop, step = 1) {
  return Array.from({ length: 1 + (stop - start) / step }, (_, i) => start + (i*step))
}
range(0, 5) // [0,1,2,3,4,5]

分支結構

let condtion = 1;

// if-else
if ( condition ) {

} else {

}

// switch-case
switch( condition ) {
  case 0: /* do something */ break;
  case 1: /* do something */ break;
  case 2: /* do something */ break;
  default: break;
}

迴圈結構


let i = 0, j = 0, k = 0;
while( i < 100 ) {
  /* do something */

  if ( j == 50 ) continue;

  if ( k == 60 ) break;

  ++i;
}

for(let i = 0 ; i < 100 ; ++i) {
  /* dosomething */
}

迭代器

let obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  key: 5,
  index: 6,
};

/* for ... in 存取物件上的 enumable 屬性 */
for(let key in obj) {} // --- s1
for(const key in obj) {
  console.log(key) // 依序印出 'a' 'b' 'c' 'd' 'key' 'index'
}

/* for ... of 存取擁有迭代器介面之物件的內容 */
let arr = [1,2,3,4,5,6];
for(let value of arr) {}
for(const value of arr) { // --- s2
  console.log(value); // 依序印出 1 2 3 4 5 6
}

Warning

雖然可以使用 let 或是 const 來宣告 for...in 與 for...of 存取的屬性 / 值 (如 s1, s2 標示的部分)

但建議都使用 const 來存取, 因為存取到變數通常是拷貝的值, 而不是副本(C++的 references &)

除非是[{}, {}, {}] 此種內容為物件型別的陣列, 才可能通過 value.property = value 進行修改, 但是也不會修改到 value 本身的參考

因此使用 const 處理迭代器是較好的選擇

錯誤處理

try {
  throw new Error("Error");
} catch (error) {
  console.error(error);
}

/* catch 的 error 可以省略 */
try {
  throw new Error("Error");
} catch {
  console.error("something wrong");
}

以上就是 JavaScript 的常用語法, 大部分都是 C-style 的樣子

let 與 var 宣告變數

1

在 JavaScript 的宣告中, 有一個特殊規則 "提升"(hoist)

Quote

變數和函數的宣告會在編譯階段就被放入記憶體, 但實際位置和程式碼中完全一樣

但是實際行為, 就像是把宣告移動到頂層區域

舉個例子來說:

function sayHello(name) {
  console.log(`${name} say: Hello!`);
}

sayHello("Alex");

以及

sayHello("Alex");

function sayHello(name) {
  console.log(`${name} say: Hello!`);
}

兩個例子, 程式都可以順利運作;對於 C 語言的開發人員, 可能不會覺得很特殊

畢竟本來就可以先宣告函式原型, 在宣告實作

但是以下的例子, 就顯得更特殊一點:

num = 7;
num = num2 + 1;
var num;
console.log(num);

Hoist1

這個例子中, "看起來"好像跟預期的結果一樣, Runtime的時候告訴, 並沒有宣告 num2 這個變數, 但是修改一樣範例程式碼:

num = 7;
num = num2 + 1;
var num, num2; //多宣告 num2
console.log(num);

Hoist2

令人驚訝的事情出現了, 此時並沒有擲出 ReferenceError, 程式碼的行為就如同:

var num, num2;
num = 7;
num = num2 + 1;
console.log(num);

因為就像是把宣告給"提升"到區域的最前面, 所以該行為才被如此稱呼

對於使用 var 來宣告的變數, 所有的宣告都被提至函數的最前面, 請注意, 僅有宣告, 初始化的行為還是在原本的位置:

var x = 1;
console.log(x , y) // 1 undefined
var y = 2;

換句話說, 該語法可以解釋成:

var x;
var y;

x = 1;
console.log(x, y);
y = 2;

正如 MDN 宣稱的

Quote

變數和函數的宣告會在編譯階段就被放入記憶體, 但實際位置和程式碼中完全一樣

這個行為算是該語言的歷史包袱, 而在ES6之後, 則出現了 letconst 關鍵字

有何不同

最大的差異在於, var 的宣告基於 函數(function), letconst 的宣告基於 區塊(block)

舉出兩個例子:

// Example 1
function foo() {
  {
    /* scope 1 */
    var a = 10;
  }

  {
    /* scope 2 */
    var b = a + 10;
  }

  console.log( b );
}
foo() // output: 20

vs

// Example 2
function foo() {
  {
    /* scope 1 */
    let a = 10;
  }

  {
    /* scope 2 */
    let b = a + 10; // ReferenceError:a is not defined
  }

  console.log( b );
}
foo() // ReferenceError:a

兩個行為非常明顯, 例子2無須多做解釋, 其行為與一般程式撰寫的邏輯相同

而例子一, 因為var 基於函數的範圍, 因此可以理解成:

function foo() {
  var a;
  var b;
  {
    /* scope 1 */
    a = 10;
  }

  {
    /* scope 2 */
    b = a + 10;
  }

  console.log( b );
}
foo() // output: 20

因此只要在函數底下的任何區塊, 使用 var 宣告, 該變數在整個函數內都可以看見, 一個簡單的處理方式是使用 IIFE(Immediately Invoked Function Expression)

function foo() {
  (function(){
    /* scope 1 */
    var a = 10;
  })();

  (function(){
    /* scope 2 */
    var b = a + 10; // ReferenceError:a is not defined
  })();

  console.log( b );
}
foo() // ReferenceError:a

透過包裹一層立即調用的函數, 把變數給隔離起來

既然問題可以解決, 那麼為什麼還需要letconst 呢?

就是為了讓 JavaScript 的行為, 更符合現代語言, 且 let 還導入了暫時性死區(TDZ) 的概念, 防止一個變數在使用前, 被其他函數或變數引用

i = j + 10; // 可以運行
var i ,j;

x = y + 10; // ReferenceError
let x, y;

此外, var 宣告很容易沒有注意到, 就錯誤的使用, 例如在HTML的 <script></script> 撰寫如下的程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
<script>
/* some javascript code */
for(var i = 0; i < n; ++i) { // <-- for 是一個 block, 不是 function, 隱含的宣告了全域變數 i
  /* do something */
}

let j = i + 1 // j 的值相當於 i 離開 for迴圈時的值 +1
</script>
</html>

如上圖所示, 或者是在 function 宣告時, 底下只要不小心使用到 var 宣告, 該變數馬上就會變成該函數內可見的變數, 另一個原因是, 在瀏覽器的行為中, Global區塊使用var 宣告的變數, 會被掛載到 window 物件底下:

browser-var

因此, 在現代的 JavaScript 撰寫中, 請盡可能避免 var 的使用