2026年11月2日 星期一

JavaScript 入門教學(四):ES6+ 現代語法(解構、展開運算子、模組)

ES6(ECMAScript 2015)是 JavaScript 歷史上最大的一次更新,之後每年也持續加入新功能,這些統稱為 ES6+。前面幾篇我們已經用過其中一部分,例如 let/const、箭頭函式、樣板字面值與 async/await。這一篇要介紹另外幾個最實用的現代語法:解構、展開運算子、物件簡寫、可選串連、模組等,讓你的程式碼更簡潔、更好讀。

一、解構賦值(Destructuring)

解構可以把陣列或物件中的值,一次拆出來指定給多個變數。

1. 陣列解構

const colors = ["紅", "綠", "藍"];

// 舊寫法
// const first = colors[0];
// const second = colors[1];

// 解構寫法
const [first, second, third] = colors;
console.log(first, second, third);   // 紅 綠 藍

// 略過不需要的項目
const [, , last] = colors;
console.log(last);                   // 藍

// 交換兩個變數的值
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b);                   // 2 1

2. 物件解構

const student = { name: "小明", age: 20, city: "台北" };

const { name, age } = student;
console.log(name, age);              // 小明 20

// 改名稱:把 city 取出並命名為 location
const { city: location } = student;
console.log(location);               // 台北

// 設定預設值
const { score = 60 } = student;
console.log(score);                  // 60(student 沒有 score,使用預設值)

3. 函式參數解構

這是實務上最常見的用法,特別適合參數很多的函式:

function showUser({ name, age = 18 }) {
  console.log(`${name},${age} 歲`);
}

showUser({ name: "小華", age: 25 });  // 小華,25 歲
showUser({ name: "小美" });           // 小美,18 歲

二、展開運算子與其餘參數(...)

三個點 ... 在不同位置有不同意思:放在「使用端」是展開,放在「接收端」是收集(其餘參數)。

1. 展開陣列與物件

// 合併陣列
const arr1 = [1, 2, 3];
const arr2 = [4, 5];
const merged = [...arr1, ...arr2];
console.log(merged);                 // [1, 2, 3, 4, 5]

// 複製陣列(淺層複製)
const copy = [...arr1];

// 合併物件,後面的會覆蓋前面相同的屬性
const defaults = { theme: "light", lang: "zh-TW" };
const custom   = { theme: "dark" };
const settings = { ...defaults, ...custom };
console.log(settings);               // { theme: "dark", lang: "zh-TW" }

// 更新物件中的某個屬性,同時保留原物件不變
const user = { name: "小明", age: 20 };
const olderUser = { ...user, age: 21 };
console.log(user.age, olderUser.age);  // 20 21

2. 其餘參數

// 把剩下的參數收集成陣列
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4));        // 10

// 搭配解構使用
const [head, ...rest] = [10, 20, 30, 40];
console.log(head);                   // 10
console.log(rest);                   // [20, 30, 40]

提醒:展開運算子做的是「淺層複製」,如果物件裡還有巢狀的物件或陣列,內層仍然是共用同一份參照。

三、物件的簡寫語法

const name = "小明";
const age = 20;

// 屬性簡寫:名稱與變數相同時可省略
const student = { name, age };

// 方法簡寫
const obj = {
  sayHi() {
    console.log("Hi!");
  }
};

// 計算屬性名稱:用 [] 動態決定屬性名
const key = "score";
const data = { [key]: 95, [`${key}Level`]: "A" };
console.log(data);                   // { score: 95, scoreLevel: "A" }

四、可選串連(?.)與空值合併(??)

讀取深層屬性時,只要中間有一層是 null 或 undefined,程式就會出錯。這兩個運算子就是為了解決這類問題:

const user = { name: "小明", address: null };

// 沒有可選串連:會出現 TypeError
// console.log(user.address.city);

// 有可選串連:遇到 null/undefined 就停止,回傳 undefined
console.log(user.address?.city);     // undefined

// 也可用於陣列與函式呼叫
console.log(user.hobbies?.[0]);      // undefined
console.log(user.sayHi?.());         // undefined(不會出錯)

// 空值合併 ??:只有左邊是 null 或 undefined 時,才使用右邊的值
const city = user.address?.city ?? "未填寫";
console.log(city);                   // 未填寫

?? 與 || 的差異

const count = 0;

console.log(count || 10);   // 10(|| 會把 0、""、false 都當成「假值」)
console.log(count ?? 10);   // 0 (?? 只在 null / undefined 時才取右邊)

當 0、空字串、false 是有效的值時,請使用 ??,才不會誤判。

五、實用的陣列與字串新方法

const nums = [5, 12, 8, 130, 44];

console.log(nums.includes(8));              // true
console.log(nums.find(n => n > 10));        // 12(第一個符合的)
console.log(nums.findIndex(n => n > 10));   // 1
console.log(nums.some(n => n > 100));       // true(只要有一個符合)
console.log(nums.every(n => n > 1));        // true(全部都符合)

// 攤平巢狀陣列
console.log([1, [2, [3]]].flat(2));         // [1, 2, 3]

// 字串方法
console.log("Hello".padStart(8, "*"));      // ***Hello
console.log("  JS  ".trim());               // JS
console.log("a-b-c".replaceAll("-", "+"));  // a+b+c

// 物件轉換
const obj = { a: 1, b: 2 };
console.log(Object.keys(obj));              // ["a", "b"]
console.log(Object.values(obj));            // [1, 2]
console.log(Object.entries(obj));           // [["a", 1], ["b", 2]]

六、模組(Modules)

當程式越寫越大,把所有程式碼放在同一個檔案會很難維護。模組讓你把功能拆成多個檔案,再用 export 匯出、import 匯入。

math.js(匯出)

// 具名匯出(可以有多個)
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

// 預設匯出(每個檔案只能有一個)
export default function multiply(a, b) {
  return a * b;
}

main.js(匯入)

// 預設匯出不用大括號;具名匯出要用大括號
import multiply, { PI, add } from "./math.js";

console.log(add(2, 3));        // 5
console.log(multiply(2, 3));   // 6
console.log(PI);               // 3.14159

index.html(載入模組)

<script type="module" src="main.js"></script>

注意:瀏覽器基於安全性,不允許直接用 file:// 開啟的網頁載入模組,必須透過本機伺服器執行。最簡單的方式是在 VS Code 安裝「Live Server」擴充功能,或在資料夾中執行 npx serve。

七、綜合範例:購物車小計

把本篇的多個語法組合起來,在 Console 中就可以直接執行:

const cart = [
  { name: "鍵盤", price: 1200, qty: 1 },
  { name: "滑鼠", price: 600,  qty: 2 },
  { name: "隨身碟", price: 300, qty: 3, discount: 50 }
];

// 計算每個品項的小計(解構 + 預設值 + 箭頭函式)
const withSubtotal = cart.map(({ name, price, qty, discount = 0 }) => ({
  name,
  subtotal: (price - discount) * qty
}));

console.log(withSubtotal);

// 加總(reduce + 解構)
const total = withSubtotal.reduce((sum, { subtotal }) => sum + subtotal, 0);
console.log(`總金額:${total} 元`);

// 新增品項,但不修改原陣列(展開運算子)
const newCart = [...cart, { name: "螢幕", price: 4500, qty: 1 }];
console.log(cart.length, newCart.length);   // 3 4

// 安全讀取可能不存在的資料(?. 與 ??)
const coupon = undefined;
console.log(coupon?.code ?? "無優惠碼");

八、練習題

  1. 使用解構,從 { title: "JS 入門", author: { name: "小明" } } 一次取出 title 與 author.name。(提示:巢狀解構)
  2. 寫一個函式 max(...nums),使用其餘參數回傳最大值。
  3. 使用展開運算子,寫出「新增一位學生到陣列、且不改動原陣列」的程式。
  4. 把第二篇的待辦清單改寫成兩個模組:storage.js 負責資料處理,main.js 負責畫面。

結語

解構、展開運算子、可選串連與模組,都是現在的前端專案(React、Vue 等框架)幾乎天天會用到的語法。只要多寫幾次,就會發現程式碼變得更短、更清楚。到這裡,這個 JavaScript 入門系列的四個主題就告一段落了:基礎語法、DOM 與事件、非同步、ES6+。接下來可以挑一個小專案(例如天氣查詢、記帳本)把這些技巧整合起來練習。如有任何問題,歡迎在下方留言討論!

沒有留言:

張貼留言