JavaScript for impatient programmers (ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

7 語法



7.1 JavaScript 語法的概觀

這是 JavaScript 語法的初探。別擔心,如果有些東西現在還看不懂。本書稍後會詳細說明。

這個概觀也不是詳盡的。它著重於重點。

7.1.1 基本結構

7.1.1.1 註解
// single-line comment

/*
Comment with
multiple lines
*/
7.1.1.2 基本(原子)值

布林值

true
false

數字

1.141
-123

基本數字類型用於浮點數(雙精度)和整數。

大整數

17n
-49n

基本數字類型只能正確表示符號位元長度在 53 位元內的整數。大整數可以任意大。

字串

'abc'
"abc"
`String with interpolated values: ${256} and ${true}`

JavaScript 沒有額外的字元類型。它使用字串來表示字元。

7.1.1.3 斷言

斷言描述計算結果預期會是什麼,如果預期不正確,就會擲回例外。例如,以下斷言指出計算結果 7 加 1 必須是 8

assert.equal(7 + 1, 8);

assert.equal() 是方法呼叫(物件是 assert,方法是 .equal()),有兩個參數:實際結果和預期結果。它是 Node.js 斷言 API 的一部分,本書稍後會說明。

還有 assert.deepEqual(),用於深入比較物件。

7.1.1.4 記錄到主控台

記錄到瀏覽器或 Node.js 的主控台

// Printing a value to standard out (another method call)
console.log('Hello!');

// Printing error information to standard error
console.error('Something went wrong!');
7.1.1.5 運算子
// Operators for booleans
assert.equal(true && false, false); // And
assert.equal(true || false, true); // Or

// Operators for numbers
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);

// Operators for bigints
assert.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);

// Operators for strings
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');

// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);

JavaScript 也有 == 比較運算子。我建議避免使用它,原因說明於§13.4.3「建議:總是使用嚴格相等」

7.1.1.6 宣告變數

const 建立不可變變數繫結:每個變數都必須立即初始化,我們不能在稍後指定不同的值。不過,值本身可能是可變的,我們可以變更它的內容。換句話說:const 沒有讓值變為不可變。

// Declaring and initializing x (immutable binding):
const x = 8;

// Would cause a TypeError:
// x = 9;

let 建立可變變數繫結

// Declaring y (mutable binding):
let y;

// We can assign a different value to y:
y = 3 * 5;

// Declaring and initializing z:
let z = 3 * 5;
7.1.1.7 一般函式宣告
// add1() has the parameters a and b
function add1(a, b) {
  return a + b;
}
// Calling function add1()
assert.equal(add1(5, 2), 7);
7.1.1.8 箭頭函式表示式

箭頭函式表示式特別用於函式呼叫和方法呼叫的參數

const add2 = (a, b) => { return a + b };
// Calling function add2()
assert.equal(add2(5, 2), 7);

// Equivalent to add2:
const add3 = (a, b) => a + b;

前一個程式碼包含以下兩個箭頭函式(術語表示式陳述式會在本章稍後說明)

// An arrow function whose body is a code block
(a, b) => { return a + b }

// An arrow function whose body is an expression
(a, b) => a + b
7.1.1.9 一般物件
// Creating a plain object via an object literal
const obj = {
  first: 'Jane', // property
  last: 'Doe', // property
  getFullName() { // property (method)
    return this.first + ' ' + this.last;
  },
};

// Getting a property value
assert.equal(obj.first, 'Jane');
// Setting a property value
obj.first = 'Janey';

// Calling the method
assert.equal(obj.getFullName(), 'Janey Doe');
7.1.1.10 陣列
// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
assert.equal(arr.length, 3);

// Getting an Array element
assert.equal(arr[1], 'b');
// Setting an Array element
arr[1] = 'β';

// Adding an element to an Array:
arr.push('d');

assert.deepEqual(
  arr, ['a', 'β', 'c', 'd']);
7.1.1.11 控制流程陳述式

條件式陳述式

if (x < 0) {
  x = -x;
}

for-of 迴圈

const arr = ['a', 'b'];
for (const element of arr) {
  console.log(element);
}
// Output:
// 'a'
// 'b'

7.1.2 模組

每個模組都是一個單一檔案。例如,考慮以下兩個包含模組的檔案

file-tools.mjs
main.mjs

file-tools.mjs 中的模組匯出其函式 isTextFilePath()

export function isTextFilePath(filePath) {
  return filePath.endsWith('.txt');
}

main.mjs 中的模組匯入整個模組 path 和函式 isTextFilePath()

// Import whole module as namespace object `path`
import * as path from 'path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';

7.1.3 類別

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `Person named ${this.name}`;
  }
  static logNames(persons) {
    for (const person of persons) {
      console.log(person.name);
    }
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)');

7.1.4 例外處理

function throwsException() {
  throw new Error('Problem!');
}

function catchesException() {
  try {
    throwsException();
  } catch (err) {
    assert.ok(err instanceof Error);
    assert.equal(err.message, 'Problem!');
  }
}

注意

變數名稱和屬性名稱的語法範疇稱為識別碼

識別碼允許包含下列字元

有些字詞在 JavaScript 中具有特殊意義,稱為保留字。範例包括:iftrueconst

保留字不能用作變數名稱

const if = 123;
  // SyntaxError: Unexpected token if

但它們可以用作屬性名稱

> const obj = { if: 123 };
> obj.if
123

7.1.6 大小寫樣式

串接字詞的常見大小寫樣式為

7.1.7 名稱大小寫

一般而言,JavaScript 使用駝峰式大小寫,常數除外。

小寫

大寫

7.1.8 更多命名慣例

下列命名慣例在 JavaScript 中很受歡迎。

如果參數名稱以底線開頭(或為底線),表示此參數未被使用,例如

arr.map((_x, i) => i)

如果物件屬性名稱以底線開頭,則該屬性被視為私人屬性

class ValueWrapper {
  constructor(value) {
    this._value = value;
  }
}

7.1.9 分號要放在哪裡?

在陳述式的結尾

const x = 123;
func();

但如果該陳述式以大括號結尾則不適用

while (false) {
  // ···
} // no semicolon

function func() {
  // ···
} // no semicolon

不過,在這種陳述式後加上分號並非語法錯誤,它會被解釋為空陳述式

// Function declaration followed by empty statement:
function func() {
  // ···
};

  測驗:基礎

請參閱 測驗應用程式

7.2 (進階)

本章節中其餘所有部分皆為進階。

7.3 識別碼

7.3.1 有效的識別碼(變數名稱等)

第一個字元

後續字元

範例

const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;

7.3.2 保留字

保留字不能作為變數名稱,但可以用作屬性名稱。

所有 JavaScript 關鍵字 都是保留字:

await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield

下列代碼也是關鍵字,但目前未用於語言中

enum implements package protected interface private public

下列字面值為保留字

true false null

技術上來說,這些字詞並非保留字,但您也應避免使用它們,因為它們實際上是關鍵字

Infinity NaN undefined async

您也不應將全域變數名稱(StringMath 等)用於自己的變數和參數。

7.4 陳述式與表達式

在本節中,我們將探討 JavaScript 如何區分兩種語法結構:陳述式表達式。之後,我們將看到這可能會造成問題,因為相同的語法在不同的使用位置可能表示不同的意義。

  我們假設只有陳述式和表達式

為了簡化起見,我們假設 JavaScript 中只有陳述式和表達式。

7.4.1 陳述式

陳述式 是可以執行並執行某種動作的程式碼片段。例如,if 是陳述式

let myStr;
if (myBool) {
  myStr = 'Yes';
} else {
  myStr = 'No';
}

另一個陳述式範例:函式宣告。

function twice(x) {
  return x + x;
}

7.4.2 表達式

表達式 是可以評估為產生值的程式碼片段。例如,括號中的程式碼是一個表達式

let myStr = (myBool ? 'Yes' : 'No');

括號中使用的運算子 _?_:_ 稱為三元運算子。它是 if 敘述的表達式版本。

讓我們來看更多表達式的範例。我們輸入表達式,而 REPL 會為我們評估它們

> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true

7.4.3 哪些地方允許什麼?

JavaScript 原始碼中的目前位置決定了你可以使用哪種語法結構

不過,表達式可以用作敘述。然後它們稱為表達式敘述。反之則不然:當內容需要表達式時,你不能使用敘述。

以下程式碼示範任何表達式 bar() 可以是表達式或敘述,這取決於內容

function f() {
  console.log(bar()); // bar() is expression
  bar(); // bar(); is (expression) statement  
}

7.5 含糊的語法

JavaScript 有幾個語法上含糊的程式結構:相同的語法會以不同的方式詮釋,這取決於是在敘述內容或表達式內容中使用。本節探討這種現象及其造成的陷阱。

7.5.1 相同的語法:函數宣告和函數表達式

函數宣告是一個敘述

function id(x) {
  return x;
}

函數表達式是一個表達式(= 的右側)

const id = function me(x) {
  return x;
};

7.5.2 相同的語法:物件文字和區塊

在以下程式碼中,{}物件文字:建立空物件的表達式。

const obj = {};

這是空的程式碼區塊(一個敘述)

{
}

7.5.3 消歧

含糊性只在敘述內容中會造成問題:如果 JavaScript 解析器遇到含糊的語法,它不知道這是一個單純的敘述還是表達式敘述。例如

為了消除含糊性,以 function{ 開頭的敘述永遠不會被詮釋為表達式。如果你想要一個表達式敘述以其中一個代幣開頭,你必須用括號將它包起來

(function (x) { console.log(x) })('abc');

// Output:
// 'abc'

在這個程式碼中

  1. 我們首先透過函數表達式建立一個函數

    function (x) { console.log(x) }
  2. 然後我們呼叫那個函數:('abc')

顯示在 (1) 中的程式碼片段只會被詮釋為表達式,因為我們用括號將它包起來。如果我們沒有這麼做,我們會得到一個語法錯誤,因為 JavaScript 會期待一個函數宣告,並抱怨函數名稱遺失。此外,你不能在函數宣告之後立即放置函數呼叫。

在本書的後續章節中,我們將看到更多由語法含糊性造成的陷阱範例

7.6 分號

7.6.1 分號的經驗法則

每個陳述句以分號結尾

const x = 3;
someFunction('abc');
i++;

除了以區塊結尾的陳述句

function foo() {
  // ···
}
if (y > 0) {
  // ···
}

以下情況稍有棘手

const func = () => {}; // semicolon!

整個 const 宣告(一個陳述句)以分號結尾,但其內部有一個箭頭函數表達式。也就是說,本身並非以大括號結尾的陳述句;而是嵌入的箭頭函數表達式。這就是為什麼結尾會有分號。

7.6.2 分號:控制陳述句

控制陳述句的主體本身就是一個陳述句。例如,以下是 while 迴圈的語法

while (condition)
  statement

主體可以是一個單一陳述句

while (a > 0) a--;

但區塊也是陳述句,因此是控制陳述句的合法主體

while (a > 0) {
  a--;
}

如果你希望迴圈有一個空的主體,你的第一個選項是一個空陳述句(只是一個分號)

while (processNextItem() > 0);

你的第二個選項是一個空區塊

while (processNextItem() > 0) {}

7.7 自動分號插入 (ASI)

雖然我建議總是寫分號,但大多數分號在 JavaScript 中都是可選的。讓這成為可能的機制稱為自動分號插入 (ASI)。在某種程度上,它會更正語法錯誤。

ASI 的運作方式如下。陳述句的分析會持續到出現以下情況為止

換句話說,ASI 可以視為在換行符號處插入分號。以下小節涵蓋了 ASI 的陷阱。

7.7.1 ASI 意外觸發

關於 ASI 的好消息是,如果你不依賴它並總是寫分號,你只需要注意一個陷阱。那就是 JavaScript 禁止在某些令牌之後換行。如果你確實插入換行符號,也會插入分號。

在這個方面最切實相關的令牌是 return。例如,考慮以下程式碼

return
{
  first: 'jane'
};

此程式碼會分析為

return;
{
  first: 'jane';
}
;

也就是說

為什麼 JavaScript 會這樣做?它可以防止在 return 之後的行中意外回傳值。

7.7.2 ASI 意外未觸發

在某些情況下,ASI 在你認為應該觸發時不會觸發。這讓不喜歡分號的人的生活變得更複雜,因為他們需要了解這些情況。以下有三個範例。還有更多。

範例 1:意外的函數呼叫。

a = b + c
(d + e).print()

分析為

a = b + c(d + e).print();

範例 2:意外的除法。

a = b
/hi/g.exec(c).map(d)

分析為

a = b / hi / g.exec(c).map(d);

範例 3:意外的屬性存取。

someFunction()
['ul', 'ol'].map(x => x + x)

執行為

const propKey = ('ul','ol'); // comma operator
assert.equal(propKey, 'ol');

someFunction()[propKey].map(x => x + x);

7.8 分號:最佳實務

我建議你總是寫分號

不過,也有許多人討厭分號帶來的額外視覺雜訊。如果您是其中之一:沒有分號的程式碼合法的。我建議您使用工具來幫助您避免錯誤。以下兩個範例

7.9 嚴格模式與隨意模式

從 ECMAScript 5 開始,JavaScript 有兩種 JavaScript 可以執行的模式

在幾乎總是位於模組中的現代 JavaScript 程式碼中,您很少會遇到隨意模式。在本書中,我假設嚴格模式總是開啟。

7.9.1 開啟嚴格模式

在腳本檔案和 CommonJS 模組中,您可以在第一行放置以下程式碼,為整個檔案開啟嚴格模式

'use strict';

這個「指令」很特別的地方在於,5 之前的 ECMAScript 版本會直接忽略它:它是一個什麼都不做的表達式陳述式。

您也可以只為單一函數開啟嚴格模式

function functionInStrictMode() {
  'use strict';
}

7.9.2 嚴格模式的改善

讓我們看看嚴格模式比隨意模式做得更好的三件事。僅在此一節中,所有程式碼片段都在隨意模式中執行。

7.9.2.1 隨意模式陷阱:變更未宣告的變數會建立一個全域變數

在非嚴格模式中,變更未宣告的變數會建立一個全域變數。

function sloppyFunc() {
  undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);

嚴格模式做得更好,會擲出 ReferenceError。這樣可以更容易偵測到拼寫錯誤。

function strictFunc() {
  'use strict';
  undeclaredVar2 = 123;
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'undeclaredVar2 is not defined',
  });

assert.throws() 陳述其第一個引數(一個函數)在呼叫時會擲出 ReferenceError

7.9.2.2 在嚴格模式中,函數宣告是區塊作用域,在隨意模式中是函數作用域

在嚴格模式中,透過函數宣告建立的變數只存在於最內層的封閉區塊中

function strictFunc() {
  'use strict';
  {
    function foo() { return 123 }
  }
  return foo(); // ReferenceError
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'foo is not defined',
  });

在隨意模式中,函數宣告是函數作用域

function sloppyFunc() {
  {
    function foo() { return 123 }
  }
  return foo(); // works
}
assert.equal(sloppyFunc(), 123);
7.9.2.3 變更不可變資料時,隨意模式不會擲出例外

在嚴格模式中,如果您嘗試變更不可變資料,您會收到一個例外

function strictFunc() {
  'use strict';
  true.prop = 1; // TypeError
}
assert.throws(
  () => strictFunc(),
  {
    name: 'TypeError',
    message: "Cannot create property 'prop' on boolean 'true'",
  });

在隨便模式中,指定會靜默失敗

function sloppyFunc() {
  true.prop = 1; // fails silently
  return true.prop;
}
assert.equal(sloppyFunc(), undefined);

  進一步閱讀:隨便模式

有關隨便模式與嚴格模式有何不同的更多資訊,請參閱 MDN

  測驗:進階

請參閱 測驗應用程式