CHAPTER 03 / INTERACTION

JavaScript:
網頁的靈魂

HTML 蓋骨架、CSS 上裝潢,但網站還像個假人——按鈕按了沒反應、表單送了沒結果。 JavaScript 就是讓這個假人活起來的咒語。它是這三章裡最難的,也是最值錢的。 這章從零教到能寫 React、Vue 的程度(含閉包、this、Promise、ES6 module 等進階主題)。

  • 能讓網站對使用者操作有反應(按鈕、表單、動畫)
  • 會用 fetch 跟 API 串接、處理非同步
  • 會用 localStorage 存使用者偏好
  • 看懂任何 JS 程式碼(閉包、this、callback、Promise、async/await)
  • 準備好可以無痛接 React 或 Vue
LESSON 3.1

JS 是什麼?怎麼開始寫?

JavaScript 是真正的程式語言。它能做運算、做判斷、做迴圈、操作 HTML、跟伺服器溝通。基本上瀏覽器能做的事都是它在做。

// 方法一:寫在 HTML 裡(小範例可以)
<script>
  alert('哈囉!');
</script>

// 方法二:外部檔案(正規做法)
<script src="app.js" defer></script>

<script> 標籤的位置很重要!放 <body> 最後面,或加 defer。否則 JS 會抓不到還沒載入的 HTML 元素。

主控台(Console)

學 JS 第一個朋友:console.log()。F12 → Console 分頁就看得到輸出。

console.log('我活著');
console.log(1 + 1);  // 2
LESSON 3.2

變數與資料型別

let name = '小明';
const age = 15;
let isStudent = true;

能用 const 就用 const,要改的時候才用 let。會讓 bug 少非常多。

七大資料型別

型別例子
String'哈囉'"哈囉"
Number423.14
Booleantruefalse
nullnull(明確的「沒有」)
undefinedundefined(還沒給值)
Array[1, 2, 3]
Object{ name: '小明', age: 15 }

樣板字串

const name = '小明';
const age = 15;

// 反引號 ` 不是單引號
const msg = `大家好我是${name},今年${age}歲`;

陣列與物件

const fruits = ['蘋果', '香蕉', '芒果'];
fruits[0];      // '蘋果'(從 0 開始!)
fruits.length;  // 3
fruits.push('葡萄');
fruits.pop();

const user = {
  name: '小明',
  age: 15,
  hobbies: ['寫程式', '打球']
};
user.name;  // '小明'

解構賦值(重要!)

// 物件解構
const { name, age } = user;
// 等同 const name = user.name; const age = user.age;

// 陣列解構
const [first, second] = fruits;

// 改名 + 預設值
const { name: userName, role = 'guest' } = user;

展開運算子 ...

// 複製陣列
const copy = [...fruits];

// 合併
const all = [...fruits, '西瓜', ...moreFruits];

// 物件複製 + 修改
const updated = { ...user, age: 16 };
LESSON 3.3

運算子與條件判斷

// 比較
5 > 3      // true
5 === 5    // true(嚴格相等)
5 !== 3    // true

// 邏輯
true && false  // false
true || false  // true

=== 不要用 ==== 會做奇怪的型別轉換,例如 '5' == 5true=== 才是「真正的相等」。

if / else / 三元

if (score >= 90) {
  console.log('A');
} else if (score >= 80) {
  console.log('B');
} else {
  console.log('C');
}

// 三元運算子
const msg = age >= 18 ? '成年' : '未成年';

短路運算(現代寫法)

// 預設值(||)
const name = userInput || '匿名';

// nullish 預設(??)只在 null/undefined 才用預設
const count = data.count ?? 0;
// 注意:0 || 1 = 1(0 被當 falsy)
// 但 0 ?? 1 = 0(0 不是 null)

// 安全存取(?.)避免 null 報錯
const city = user?.address?.city;
LESSON 3.4

迴圈與陣列方法

for (let i = 0; i < 5; i++) {
  console.log(i);
}

// for...of(推薦)
for (const fruit of fruits) {
  console.log(fruit);
}

陣列三神器:map / filter / reduce

會用這三個你在 React、Vue 都吃得開。

const nums = [1, 2, 3, 4, 5];

// map:每個元素變身
const doubled = nums.map(n => n * 2);
// [2, 4, 6, 8, 10]

// filter:留下符合條件的
const evens = nums.filter(n => n % 2 === 0);
// [2, 4]

// reduce:濃縮成一個值
const sum = nums.reduce((total, n) => total + n, 0);
// 15

其他常用方法

fruits.find(f => f === '蘋果');   // 找第一個符合的
fruits.some(f => f.length > 3);    // 有任何一個符合?
fruits.every(f => f.length > 0);   // 全部都符合?
fruits.includes('蘋果');          // 包含?
fruits.sort();                      // 排序
fruits.reverse();                   // 反轉
[...fruits, ...veggies].join(', ');  // 合併並轉字串
LESSON 3.5

函式

// 寫法一:function 宣告
function greet(name) {
  return `哈囉,${name}!`;
}

// 寫法二:箭頭函式(現代推薦)
const greet = (name) => {
  return `哈囉,${name}!`;
};

// 短版
const greet = name => `哈囉,${name}!`;

// 預設參數
const greet = (name = '路人') => `嗨 ${name}`;

// 剩餘參數
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
sum(1, 2, 3, 4);  // 10
LESSON 3.6

進階:作用域與閉包

到這裡很多新手想跳過。不要跳。閉包是 JS 的靈魂,搞懂這個你就上一階。

作用域(Scope)

變數有「能被看到的範圍」。let / const區塊作用域(大括號內)。

function demo() {
  if (true) {
    let x = 1;
  }
  console.log(x);  // 錯誤!x 在 if 外面看不到
}

閉包(Closure)

函式裡面可以「記住」外面的變數,即使外面的函式已經結束。

function makeCounter() {
  let count = 0;

  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
counter();  // 1
counter();  // 2
counter();  // 3

makeCounter 已經回傳完了,但裡面的 count 沒消失,內部那個函式「閉包」住了它。

想像一個店員下班了(外層函式結束),但他口袋裡還有一張收據(閉包變數),這張收據可以被未來別人查(內層函式被呼叫)。

閉包能做什麼?

// 1. 私有變數(資料封裝)
function createWallet(initial) {
  let balance = initial;

  return {
    deposit: (n) => { balance += n; },
    withdraw: (n) => { balance -= n; },
    check: () => balance
  };
}

const wallet = createWallet(100);
wallet.deposit(50);
wallet.check();   // 150
// balance 在外面拿不到,安全


// 2. 函式工廠
function multiplier(n) {
  return (x) => x * n;
}

const double = multiplier(2);
const triple = multiplier(3);
double(5);  // 10
triple(5);  // 15
LESSON 3.7

進階:this 是什麼?

this 是 JS 最讓新手抓狂的東西。記住一個原則:this 取決於「誰呼叫這個函式」

const user = {
  name: '小明',
  greet: function() {
    console.log(`我是 ${this.name}`);
  }
};

user.greet();   // "我是 小明",this = user

const fn = user.greet;
fn();             // "我是 undefined",this = window/undefined

箭頭函式不一樣

箭頭函式沒有自己的 this,會用「定義它的位置」的 this。

const user = {
  name: '小明',
  hobbies: ['寫程式', '打球'],

  showHobbies: function() {
    this.hobbies.forEach(h => {
      // 箭頭函式,this 還是 user
      console.log(`${this.name} 喜歡 ${h}`);
    });
  }
};

規則:定義方法用 function(this 指向物件本身),裡面的 callback 用箭頭函式(this 維持外層)。React 跟 Vue 大量利用這個機制。

LESSON 3.8

DOM 操作

DOM(Document Object Model)是瀏覽器把 HTML 轉成 JS 看得懂的物件。改它網頁就會變。

// 抓元素
const title = document.getElementById('title');
const btn = document.querySelector('.btn');
const cards = document.querySelectorAll('.card');

// 改內容
title.textContent = '新標題';
title.innerHTML = '<em>斜體</em>';
img.src = 'new.jpg';

// 改樣式(推薦用 class)
box.classList.add('active');
box.classList.remove('active');
box.classList.toggle('active');

// 新增、刪除元素
const li = document.createElement('li');
li.textContent = '新項目';
list.appendChild(li);
li.remove();

innerHTML 危險!如果裡面塞使用者輸入的內容,攻擊者可以注入 <script> 偷資料(XSS 攻擊)。塞使用者輸入永遠用 textContent。詳細在第 13 章。

LESSON 3.9

事件

const btn = document.querySelector('.btn');

btn.addEventListener('click', () => {
  alert('被按了!');
});

// 取得事件資訊
input.addEventListener('input', (e) => {
  console.log(e.target.value);
});

form.addEventListener('submit', (e) => {
  e.preventDefault();  // 阻止預設行為(不要重整)
  // 處理表單
});

事件委派

有 100 個按鈕怎麼辦?不要綁 100 個監聽器。綁在父層,靠冒泡:

list.addEventListener('click', (e) => {
  if (e.target.matches('.delete-btn')) {
    e.target.closest('li').remove();
  }
});
LESSON 3.10

進階:非同步、Promise、async/await

JavaScript 是單執行緒——一次只能做一件事。但發 API 請求、讀檔案會花時間。如果都同步等,網頁會卡死。所以有了非同步機制。

callback(傳統)

setTimeout(() => {
  console.log('3 秒後出現');
}, 3000);

Promise(現代)

Promise = 「我等等會給你答案,可能成功也可能失敗」的承諾。

fetch('https://api.example.com/users')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

async / await(現代最推薦)

async function getUsers() {
  try {
    const response = await fetch('/api/users');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('失敗:', error);
  }
}

async 標記一個函式是非同步的,await 等 Promise 完成。比 .then() 易讀。

Promise.all:並行處理

// 同時發三個請求,全部完成才繼續
const [users, posts, comments] = await Promise.all([
  fetch('/users').then(r => r.json()),
  fetch('/posts').then(r => r.json()),
  fetch('/comments').then(r => r.json())
]);

POST 請求

const response = await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'a@b.com',
    password: '1234'
  })
});
LESSON 3.11

進階:ES6 Module

專案大了,所有 JS 寫一個檔會炸。Module 讓你拆成多個檔,互相 import。

// math.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
export const PI = 3.14159;

// 預設 export,每個檔最多一個
export default function main() { ... }
// app.js
import { add, sub, PI } from './math.js';
import main from './math.js';
import * as math from './math.js';

console.log(add(1, 2));

HTML 要用 module 模式:

<script type="module" src="app.js"></script>
LESSON 3.12

localStorage 與除錯

localStorage:存資料在瀏覽器

localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');

// 物件要先轉字串
localStorage.setItem('user', JSON.stringify(user));
const saved = JSON.parse(localStorage.getItem('user'));

除錯四把刀

  1. console.log()console.table()(陣列/物件用 table 看更清楚)
  2. F12 → Console 看錯誤、Network 看 API 請求、Elements 看 DOM
  3. 讀錯誤訊息,紅字會告訴你哪行錯、為什麼錯
  4. debugger; 寫在程式裡,瀏覽器會在那一行暫停讓你檢查

練習:To-Do List

JS 的「Hello World」。要包含:

  1. 輸入框 + 新增按鈕
  2. 清單顯示所有待辦事項
  3. 每項旁邊有刪除按鈕
  4. 點項目可切換完成狀態(加刪除線)
  5. 用 localStorage 存資料,重整不消失
  6. 底部顯示「還有 3 項未完成」
  7. 「清除所有完成」按鈕

常見卡關 FAQ

// JS 學員最常問的問題
為什麼 querySelector 抓不到元素?
最常見:JS 在 HTML 之前執行了。把 <script> 放 body 最後,或加 defer。次常見:選擇器寫錯,class 前要 .
為什麼點擊事件綁不上去?
通常是元素還沒存在你就綁了,或者綁錯元素。F12 → Console 看有沒有錯誤訊息。動態新增的元素要用事件委派
async function 為什麼回傳 Promise 不是值?
這是 async 的設計。你要在另一個 async 函式裡 await 它,或用 .then()。或者最外層用 IIFE:(async () => {{ const x = await fn(); }})()
setTimeout 0 跟同步差在哪?
同步是「現在」執行,setTimeout(..., 0) 是「目前同步程式跑完,下一個事件循環」執行。會被排到「微任務」之後。這個概念在進階前端面試會考。
為什麼陣列在物件裡修改不會觸發畫面更新(React/Vue)?
框架靠「引用變了」才知道要更新。arr.push(x) 是直接改原陣列,引用沒變。要 setArr([...arr, x]) 建立新陣列。這就是為什麼陣列三神器(map/filter)在框架裡這麼重要——它們都回傳新陣列。
← 上一章
02 CSS