第 4 章 JavaScript 与 TypeScript(从零开始)
本章继续假设你没基础。HTML 给了页面骨架,CSS 给了外观,这一章的 JavaScript 负责让页面会响应——点击按钮弹窗、输入搜索词过滤结果、拖进度条改音量。没有 JS 的网页就是一张贴纸。本章末尾会引入 TypeScript,它只是"给 JS 加上类型检查",避免一类粗心错误。
零、先问几个问题
JavaScript 在哪里运行?
- 浏览器里(包括 Tauri 的 WebView 窗口)——操作页面、发网络请求。
- Node.js 里(服务器或命令行工具)——读写文件、启 HTTP 服务。
本书只关心浏览器里的 JS(Tauri 前端就是这个场景)。
怎么最快试一段 JS? 打开任意浏览器,按 F12 打开开发者工具,切到 Console(控制台) 标签。里面就能直接输入一行 JS 按回车执行:
console.log("Hello"); // 控制台打印 "Hello"
1 + 2 // 回显 3
Math.random() // 回显一个 0~1 的随机数
或者在 HTML 里这样写,一行代码验证:
<!doctype html>
<html>
<body>
<h1 id="title">Hello</h1>
<button id="btn">点我</button>
<script>
document.getElementById("btn").addEventListener("click", () => {
document.getElementById("title").textContent = "被点到了!";
});
</script>
</body>
</html>
保存,双击打开。点按钮,标题就变了。这就是 JS 做的事。
一、变量:给值起名字
const name = "CloudTone"; // 不会改变的用 const
let count = 0; // 可能变化的用 let
count = count + 1; // 可以重新赋值
// const name = "别的"; // 报错:const 不能改
const= constant(常量),不能再次赋值。默认都用它。let= 可以再次赋值。确实需要变化时才用。- 别用
var,是老语法,有各种坑。
命名规则:用英文字母、数字、下划线、$,但不能以数字开头。习惯用 小驼峰 风格:userName、playCount、isLoading。
二、基本数据类型
JS 里你会频繁用到这 5 种:
const age = 25; // 数字(number)
const title = "起风了"; // 字符串(string),单双引号都行
const isPlaying = true; // 布尔(boolean),只有 true / false
const nothing = null; // "明确的空"
let u; // undefined,"还没赋值"
字符串的模板语法(比加号拼接好用 10 倍):
const name = "小明";
const age = 18;
// 老写法
const msg1 = "我叫" + name + ",今年" + age + "岁";
// 模板字符串(用反引号 ` 而不是引号)
const msg2 = `我叫${name},今年${age}岁`;
反引号里 ${} 中可以放任何表达式:
const price = 99;
const text = `总价:${price * 1.1} 元`;
三、运算符
// 算术
1 + 2 // 3
10 / 3 // 3.333...
10 % 3 // 1 取余数
2 ** 8 // 256 幂
// 比较(返回 true / false)
1 === 1 // true 严格相等,永远用这个
1 === "1" // false 类型不同
1 !== 2 // true
3 > 2 // true
3 >= 3 // true
// 逻辑
true && false // false "和",都真才真
true || false // true "或",一个真就真
!true // false "非"
// 字符串拼接
"a" + "b" // "ab"
关键规则:永远用 === 和 !==,不要用 == 和 !=。后者会做奇怪的类型转换,比如 0 == "" 竟然是 true。
四、条件判断
const age = 18;
if (age >= 18) {
console.log("成年");
} else if (age >= 12) {
console.log("青少年");
} else {
console.log("儿童");
}
三元运算符(if-else 的简写,在 React 里用得非常多):
const status = age >= 18 ? "成年" : "未成年";
// 等价于:if (age >= 18) status = "成年"; else status = "未成年";
短路语法(常见简写):
const name = userName || "匿名"; // userName 为空就用 "匿名"
const len = arr?.length ?? 0; // arr 可能不存在时取 length,没有就 0
a || b:a 是 "假值"(false/0/""/null/undefined)时取 b。a ?? b:a 是null或undefined时取 b,空字符串和 0 不算。obj?.field:obj 是 null/undefined 时直接返回 undefined,不报错。
五、数组:有序列表
const fruits = ["苹果", "香蕉", "橘子"];
fruits[0]; // "苹果",下标从 0 开始
fruits.length; // 3
fruits.push("梨"); // 末尾追加
fruits.pop(); // 弹出末尾
fruits.includes("苹果"); // true
最常用的三个数组方法(React 里天天用):
const nums = [1, 2, 3, 4];
// map:每个元素做一次变换,得到新数组
const doubled = nums.map(n => n * 2); // [2, 4, 6, 8]
// filter:只保留满足条件的元素
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
// find:找第一个满足条件的
const first = nums.find(n => n > 2); // 3
n => n * 2 是"箭头函数"(下一节讲),先知道它就是个函数。
遍历数组:
for (const fruit of fruits) {
console.log(fruit);
}
// 或者 forEach
fruits.forEach(fruit => console.log(fruit));
六、对象:带标签的数据
对象是"一堆键值对":
const song = {
id: 1,
title: "起风了",
artist: "买辣椒也用券",
duration: 321,
isLiked: true,
};
song.title; // "起风了"
song.duration; // 321
song.title = "新标题"; // 修改
song.rating = 5; // 添加新字段
对象可以嵌套:
const user = {
name: "小明",
address: {
city: "北京",
zip: "100000",
},
};
user.address.city; // "北京"
对象数组(最常见的数据结构):
const songs = [
{ id: 1, title: "歌曲 A", artist: "歌手甲" },
{ id: 2, title: "歌曲 B", artist: "歌手乙" },
{ id: 3, title: "歌曲 C", artist: "歌手丙" },
];
// 找 ID 为 2 的歌
const song = songs.find(s => s.id === 2);
// 列出所有标题
const titles = songs.map(s => s.title); // ["歌曲 A", "歌曲 B", "歌曲 C"]
// 过滤出 "歌手甲" 的歌
const filtered = songs.filter(s => s.artist === "歌手甲");
这三个操作(map、filter、find)将来你会在 React 里每天用几十次。
6.1 解构:快速提取字段
const song = { id: 1, title: "起风了", artist: "买辣椒" };
// 老写法
const id = song.id;
const title = song.title;
// 解构(推荐)
const { id, title } = song;
const { artist: singer } = song; // 顺便重命名
数组解构:
const [first, second] = [10, 20, 30]; // first=10, second=20
6.2 展开运算符 ...
const a = [1, 2, 3];
const b = [...a, 4, 5]; // [1, 2, 3, 4, 5]
const song1 = { id: 1, title: "A" };
const song2 = { ...song1, title: "B" }; // 复制 + 覆盖 title
// song2 = { id: 1, title: "B" }
展开运算符在 React 里做"不可变更新"必备。
七、函数:可复用的代码块
三种写法,功能几乎一样:
// 1. 函数声明
function add(a, b) {
return a + b;
}
// 2. 函数表达式
const add2 = function(a, b) {
return a + b;
};
// 3. 箭头函数(推荐,最简洁)
const add3 = (a, b) => a + b;
// 如果函数体多行:
const add4 = (a, b) => {
const sum = a + b;
return sum;
};
调用方式相同:
add(3, 4); // 7
add3(3, 4); // 7
默认参数:
function greet(name = "朋友") {
return `你好,${name}`;
}
greet(); // "你好,朋友"
greet("小明"); // "你好,小明"
函数是一等公民:函数可以存进变量,也可以当参数传给别的函数。这就是为什么 arr.map(n => n * 2) 能工作——map 接收一个函数参数。
function applyTwice(fn, x) {
return fn(fn(x));
}
applyTwice(n => n + 1, 5); // 7,因为 (5+1)+1
八、DOM 操作:让 JS 控制 HTML
DOM 是浏览器给 JS 的"操作界面"——每个 HTML 元素在 JS 里对应一个对象,改这个对象,页面就变。
<h1 id="title">原文字</h1>
<button id="btn">点我</button>
<ul id="list"></ul>
<script>
// 找元素
const title = document.getElementById("title");
const btn = document.getElementById("btn");
const list = document.getElementById("list");
// 改内容
title.textContent = "新文字";
// 改样式
title.style.color = "red";
title.style.fontSize = "30px";
// 加类
title.classList.add("active");
title.classList.remove("active");
title.classList.toggle("active");
// 监听点击
btn.addEventListener("click", () => {
// 动态生成一个 <li> 加进列表
const li = document.createElement("li");
li.textContent = "新的一项 " + Math.random();
list.appendChild(li);
});
</script>
把上面整段存成 index.html 打开,每次点按钮,列表里就多一项。
这就是"原生 JS 做前端"的全貌。但你会发现:
- 要精确找到每个元素(
getElementById)。 - 每次数据变了都要手动改 DOM。
- 数据多了代码很乱。
这正是 React 要解决的问题(下一章)。
8.1 其他常用选择方法
document.querySelector("#title"); // 用 CSS 选择器找第一个
document.querySelector(".card"); // 找第一个 class="card" 的
document.querySelectorAll(".card"); // 找所有,返回数组样的东西
九、事件:响应用户操作
btn.addEventListener("click", (e) => {
console.log("被点到了");
console.log("鼠标坐标:", e.clientX, e.clientY);
});
input.addEventListener("input", (e) => {
console.log("当前输入:", e.target.value);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") console.log("按了 Esc");
});
常用事件名:click、input、change、submit、keydown、mouseenter、mouseleave、focus、blur。
十、异步:处理"需要等待的事情"
问题:从服务器拉数据、读文件、等几秒、动画……这些操作不会立刻完成。JS 是单线程的,不能傻等,否则页面就卡死了。
解法:回调 → Promise → async/await。现代 JS 基本只用后两种。
10.1 先看一个卡住的例子(反例)
console.log("A");
for (let i = 0; i < 1000000000; i++) {} // 跑一秒多
console.log("B");
// 输出:A(卡一下)B
// 这期间页面完全动不了
正确的"等一下"用 setTimeout:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000); // 1000 毫秒 = 1 秒后执行
console.log("C");
// 输出:A → C → (1秒后) B
注意顺序:setTimeout 不会阻塞,它说"1 秒后再跑我给你的函数",JS 继续往下走,所以 C 在 B 之前。
10.2 Promise:描述"将来会有的结果"
想象你点外卖:下单那一刻外卖还没到,但你知道将来会到(或失败)。这就是 Promise。
// 一个 3 秒后才给出结果的 Promise
const orderFood = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.2;
if (success) {
resolve("外卖到了 🍜"); // 成功:调 resolve
} else {
reject("商家取消订单"); // 失败:调 reject
}
}, 3000);
});
// 用法
orderFood
.then(result => console.log("✓", result))
.catch(err => console.log("✗", err));
console.log("下单完成,继续干别的");
.then(f) 是"成功时跑 f",.catch(f) 是"失败时跑 f"。
真实世界的例子:fetch 就是返回 Promise 的网络请求。
fetch("https://api.github.com/users/torvalds")
.then(response => response.json())
.then(data => console.log(data.name))
.catch(err => console.error(err));
10.3 async / await:让异步代码像同步一样读
上面的写法链式调用多了会嵌套得很丑。async/await 是糖衣,让代码从上到下读:
async function loadUser() {
try {
const response = await fetch("https://api.github.com/users/torvalds");
const data = await response.json();
console.log(data.name);
} catch (err) {
console.error("出错了:", err);
}
}
loadUser();
三点必记:
- 用
async标记的函数总是返回 Promise。 await只能在async函数里用。await X会"暂停这个函数"直到 X 出结果。但不阻塞整个页面。
并发多个异步:
// ❌ 一个一个等(慢)
const songs = await fetchSongs();
const artists = await fetchArtists();
// ✓ 同时发,一起等(快)
const [songs, artists] = await Promise.all([fetchSongs(), fetchArtists()]);
10.4 一个完整的小例子
<input id="q" placeholder="输入 GitHub 用户名" />
<button id="go">搜</button>
<div id="result"></div>
<script>
const q = document.getElementById("q");
const go = document.getElementById("go");
const result = document.getElementById("result");
go.addEventListener("click", async () => {
result.textContent = "加载中...";
try {
const res = await fetch(`https://api.github.com/users/${q.value}`);
if (!res.ok) throw new Error("未找到");
const data = await res.json();
result.innerHTML = `<img src="${data.avatar_url}" width="80" /><p>${data.name}</p>`;
} catch (e) {
result.textContent = "出错: " + e.message;
}
});
</script>
动手试试 ①:把这段存成 HTML 打开,输入 torvalds 点搜索看看。
十一、模块化:代码分文件
一个项目几千行代码放一个文件不现实。JS 用 import / export 拆文件:
// utils.js
export const PI = 3.14;
export function area(r) {
return PI * r * r;
}
// 默认导出(一个文件最多一个)
export default function hello() {
return "hi";
}
// main.js
import hello, { PI, area } from "./utils.js";
console.log(PI);
console.log(area(5));
console.log(hello());
在 HTML 里用模块:
<script type="module" src="./main.js"></script>
type="module" 是关键。Vite / Tauri 项目里默认都是模块,不用你手动写。
十二、TypeScript:给 JS 加类型
12.1 为什么需要它
纯 JS 的这种问题特别常见:
function sum(nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(5); // 炸了!5 不是数组,但直到运行才发现
TypeScript 让你在写代码时就标出类型,编辑器会立即报红:
function sum(nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
sum(5); // 编辑器直接红线:参数不对
sum([1, 2, 3]); // ✓
12.2 基本类型写法
let name: string = "小明";
let age: number = 18;
let isMember: boolean = true;
let tags: string[] = ["pop", "rock"]; // 字符串数组
let ids: number[] = [1, 2, 3];
// 可能为空
let nickname: string | null = null;
let optional: number | undefined;
// 字面量联合(限定几个值之一)
let status: "idle" | "loading" | "success" | "error" = "idle";
联合类型 A | B 意思是"要么 A 要么 B"。非常常用。
12.3 对象类型:用 interface
interface Song {
id: number;
title: string;
artist: string;
duration: number;
isLiked?: boolean; // 问号表示"可选"
}
const s: Song = {
id: 1,
title: "起风了",
artist: "买辣椒",
duration: 321,
};
interface 就是"这个对象必须长什么样"的说明。
12.4 函数类型
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数
const greet = (name: string): string => `你好 ${name}`;
// 返回值可以省,让 TS 自动推断
const mult = (a: number, b: number) => a * b;
12.5 数组里装对象
const songs: Song[] = [
{ id: 1, title: "A", artist: "甲", duration: 200 },
{ id: 2, title: "B", artist: "乙", duration: 180 },
];
// map/filter 的类型自动推断
const titles = songs.map(s => s.title); // string[]
const longOnes = songs.filter(s => s.duration > 190);
12.6 泛型:参数化类型
如果一个工具"对任何类型都行",用泛型:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // number | undefined
first(["a", "b"]); // string | undefined
first<Song>([/* ... */]); // Song | undefined
<T> 里的 T 就像一个类型参数。调用时 TS 自动填它。
12.7 any 和 unknown
let x: any; // 什么都能塞,什么检查都没(逃避类型检查的逃生舱,别滥用)
let y: unknown; // 什么都能塞,但用之前必须"收窄"到具体类型
尽量别用 any,它等于放弃了 TS 的保护。
12.8 tsconfig.json 关键开关
项目根目录的 tsconfig.json 控制检查严格程度:
{
"compilerOptions": {
"target": "ES2022",
"strict": true, // 打开所有严格检查(必选)
"noUncheckedIndexedAccess": true, // arr[i] 可能是 undefined,逼你处理
"skipLibCheck": true
}
}
strict: true 这一个开关就够大部分项目用了。
十三、常见陷阱与坑
typeof null === "object":JS 历史遗留 bug,判断 null 要x === null。- 0.1 + 0.2 !== 0.3:浮点数精度问题,金额用分为单位存整数。
arr.sort()默认按字符串排:[10, 2, 1].sort()结果是[1, 10, 2]。数字要写arr.sort((a,b) => a-b)。- 对象是引用:
const a = { x: 1 }; const b = a; b.x = 2;此时a.x也是 2!要拷贝用{ ...a }。 for...in遍历 key,for...of遍历 value:别搞混。- 箭头函数没有自己的
this:在回调里想用外层this时特别好用,这也是推荐它的原因之一。
十四、一个综合小项目:TODO 列表
把本章学的全部用上,写一个最简 TODO。新建 todo.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { box-sizing: border-box; font-family: sans-serif; }
body { max-width: 400px; margin: 40px auto; padding: 20px; }
.row { display: flex; gap: 8px; margin-bottom: 16px; }
.row input { flex: 1; padding: 8px; }
.row button { padding: 8px 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid #eee; }
li.done span { text-decoration: line-through; color: #aaa; }
li button { margin-left: auto; background: #ef4444; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<h1>我的 TODO</h1>
<div class="row">
<input id="input" placeholder="要做什么?" />
<button id="add">添加</button>
</div>
<ul id="list"></ul>
<script>
const todos = [];
const input = document.getElementById("input");
const addBtn = document.getElementById("add");
const list = document.getElementById("list");
function render() {
list.innerHTML = "";
todos.forEach((todo, idx) => {
const li = document.createElement("li");
if (todo.done) li.classList.add("done");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.done;
checkbox.addEventListener("change", () => {
todos[idx].done = checkbox.checked;
render();
});
const span = document.createElement("span");
span.textContent = todo.text;
const delBtn = document.createElement("button");
delBtn.textContent = "删";
delBtn.addEventListener("click", () => {
todos.splice(idx, 1);
render();
});
li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(delBtn);
list.appendChild(li);
});
}
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
todos.push({ text, done: false });
input.value = "";
render();
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") addBtn.click();
});
</script>
</body>
</html>
打开这个文件,你能:输入任务回车添加、勾选打钩、点"删"删除。
这就是原生 JS 版的 TODO。注意每次改数据都要手动 render(),页面才更新。React 就是来解决这个烦恼的。
本章小结
- 变量:
const(不变)/let(变)。 - 类型:number / string / boolean / null / undefined / 数组 / 对象。
- 函数:三种写法功能相同,推荐箭头函数。
- 数组三件套:
map/filter/find。 - DOM 操作:
getElementById+addEventListener改textContent/style/classList。 - 异步:
async/await+try/catch处理网络请求等。 - 模块:
import/export拆代码。 - TypeScript:给变量、参数、返回值加类型标注,编辑器帮你提前抓错。
下一章:React——让你不再手动拼 DOM,用"写 UI = 写函数"的方式高效做界面。