JavaScript:
網頁的靈魂
HTML 蓋骨架、CSS 上裝潢,但網站還像個假人——按鈕按了沒反應、表單送了沒結果。 JavaScript 就是讓這個假人活起來的咒語。它是這三章裡最難的,也是最值錢的。 這章從零教到能寫 React、Vue 的程度(含閉包、this、Promise、ES6 module 等進階主題)。
- 能讓網站對使用者操作有反應(按鈕、表單、動畫)
- 會用 fetch 跟 API 串接、處理非同步
- 會用 localStorage 存使用者偏好
- 看懂任何 JS 程式碼(閉包、this、callback、Promise、async/await)
- 準備好可以無痛接 React 或 Vue
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
變數與資料型別
let name = '小明';
const age = 15;
let isStudent = true;
let:可重新賦值的變數const:一次決定不能改var:舊版的,不要用
能用 const 就用 const,要改的時候才用 let。會讓 bug 少非常多。
七大資料型別
| 型別 | 例子 |
|---|---|
| String | '哈囉' 或 "哈囉" |
| Number | 42、3.14 |
| Boolean | true 或 false |
| null | null(明確的「沒有」) |
| undefined | undefined(還沒給值) |
| 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 };
運算子與條件判斷
// 比較
5 > 3 // true
5 === 5 // true(嚴格相等)
5 !== 3 // true
// 邏輯
true && false // false
true || false // true
用 === 不要用 ==。== 會做奇怪的型別轉換,例如 '5' == 5 是 true。=== 才是「真正的相等」。
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;
迴圈與陣列方法
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(', '); // 合併並轉字串
函式
// 寫法一: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
進階:作用域與閉包
到這裡很多新手想跳過。不要跳。閉包是 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
進階: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 大量利用這個機制。
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 章。
事件
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();
}
});
進階:非同步、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'
})
});
進階: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>
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'));
除錯四把刀
console.log()、console.table()(陣列/物件用 table 看更清楚)- F12 → Console 看錯誤、Network 看 API 請求、Elements 看 DOM
- 讀錯誤訊息,紅字會告訴你哪行錯、為什麼錯
debugger;寫在程式裡,瀏覽器會在那一行暫停讓你檢查
練習:To-Do List
JS 的「Hello World」。要包含:
- 輸入框 + 新增按鈕
- 清單顯示所有待辦事項
- 每項旁邊有刪除按鈕
- 點項目可切換完成狀態(加刪除線)
- 用 localStorage 存資料,重整不消失
- 底部顯示「還有 3 項未完成」
- 「清除所有完成」按鈕
常見卡關 FAQ
<script> 放 body 最後,或加 defer。次常見:選擇器寫錯,class 前要 .。await 它,或用 .then()。或者最外層用 IIFE:(async () => {{ const x = await fn(); }})()setTimeout(..., 0) 是「目前同步程式跑完,下一個事件循環」執行。會被排到「微任務」之後。這個概念在進階前端面試會考。arr.push(x) 是直接改原陣列,引用沒變。要 setArr([...arr, x]) 建立新陣列。這就是為什麼陣列三神器(map/filter)在框架裡這麼重要——它們都回傳新陣列。