JavaScript 是现代 Web 开发的核心语言之一。它最初用于为网页添加交互能力,如今已经广泛应用于浏览器前端、服务端、桌面应用、移动应用、自动化脚本以及物联网等领域。
本文将从 JavaScript 的基本概念开始,逐步介绍变量、数据类型、函数、对象、原型、异步编程、DOM、网络请求、模块化、Node.js、性能优化和安全实践,并通过一个完整的待办事项项目串联核心知识。
1. JavaScript 是什么
JavaScript 是一种高级、动态类型、基于原型、多范式的编程语言。
它支持多种编程方式:
- 命令式编程
- 面向对象编程
- 函数式编程
- 事件驱动编程
- 异步编程
JavaScript 与 ECMAScript 经常同时出现,但二者并不完全相同:
- ECMAScript:JavaScript 语言的标准规范。
- JavaScript:ECMAScript 规范的一种实际实现,以及围绕它形成的运行时和生态系统。
- DOM、Fetch、localStorage:浏览器提供的 Web API,并不是 ECMAScript 核心语言的一部分。
- 文件、网络、进程 API:通常由 Node.js 等服务端运行时提供。
可以把一个 JavaScript 运行环境理解为:
text
JavaScript 运行环境
├── JavaScript 引擎
│ ├── 解析源代码
│ ├── 编译或即时编译
│ ├── 执行代码
│ └── 垃圾回收
├── 宿主环境 API
│ ├── 浏览器:DOM、Fetch、Storage
│ └── Node.js:文件、网络、进程
└── 任务调度机制
├── 调用栈
├── 任务队列
└── 微任务队列
JavaScript 并不只是传统意义上的"解释型语言"。现代 JavaScript 引擎通常会对代码进行解析、编译和即时优化。
2. JavaScript 的运行环境
2.1 浏览器
浏览器是学习 JavaScript 最方便的环境。
按 F12 打开开发者工具,在 Console 控制台中输入:
javascript
console.log("Hello, JavaScript!");
浏览器不仅可以执行 JavaScript,还提供了:
- DOM 页面操作
- 用户事件
- 网络请求
- 本地存储
- 定时器
- Canvas
- Web Worker
- WebSocket
2.2 Node.js
Node.js 允许 JavaScript 脱离浏览器运行,常用于:
- Web 服务端
- 命令行工具
- 自动化脚本
- 构建工具
- 服务端渲染
- 桌面应用的底层能力
创建 hello.js:
javascript
console.log("Hello from Node.js!");
执行:
bash
node hello.js
2.3 浏览器与 Node.js 的区别
| 能力 | 浏览器 | Node.js |
|---|---|---|
| JavaScript 核心语法 | 支持 | 支持 |
| DOM | 支持 | 默认不支持 |
window |
支持 | 不支持 |
document |
支持 | 不支持 |
| 文件系统 | 浏览器中受限制 | 支持 |
process |
不支持 | 支持 |
| CommonJS | 默认不支持 | 支持 |
| ES Module | 支持 | 支持 |
因此,JavaScript 代码能否跨环境运行,取决于它是否依赖特定宿主提供的 API。
3. 第一个 JavaScript 程序
创建一个 index.html 文件:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript 示例</title>
</head>
<body>
<button id="hello-button">点击我</button>
<script type="module">
const button = document.querySelector("#hello-button");
button.addEventListener("click", () => {
alert("Hello, JavaScript!");
});
</script>
</body>
</html>
示例完成了三个操作:
- 使用
document.querySelector()获取按钮。 - 使用
addEventListener()监听点击事件。 - 点击按钮后调用
alert()显示消息。
实际项目通常将 JavaScript 放在单独文件中:
html
<script type="module" src="./main.js"></script>
main.js:
javascript
const button = document.querySelector("#hello-button");
button.addEventListener("click", () => {
alert("Hello, JavaScript!");
});
使用 ES Module 时,建议通过本地 HTTP 服务打开页面,而不是直接使用 file:// 协议。
4. 变量与常量
JavaScript 可以使用 const、let 和 var 声明变量。
4.1 const
const 声明的变量不能被重新赋值:
javascript
const siteName = "JavaScript 教程";
console.log(siteName);
下面的代码会报错:
javascript
const siteName = "JavaScript 教程";
siteName = "新的名称";
但是,const 只限制变量本身不能指向另一个值,并不意味着对象内部完全不可修改:
javascript
const user = {
name: "张三",
age: 20
};
user.age = 21;
console.log(user);
如果希望冻结对象,可以使用:
javascript
const config = Object.freeze({
host: "localhost",
port: 8080
});
Object.freeze() 默认只进行浅层冻结,嵌套对象仍可能被修改。
4.2 let
let 适合声明需要重新赋值的变量:
javascript
let count = 0;
count += 1;
count += 1;
console.log(count);
4.3 var
var 是早期 JavaScript 的变量声明方式:
javascript
var message = "Hello";
它存在以下特点:
- 使用函数作用域,而不是块级作用域。
- 声明会被提升。
- 允许在同一作用域中重复声明。
- 在循环、异步回调和大型函数中容易产生误解。
示例:
javascript
if (true) {
var value = 100;
}
console.log(value); // 100
而 let 和 const 具有块级作用域:
javascript
if (true) {
const value = 100;
}
console.log(value); // ReferenceError
现代 JavaScript 推荐:
- 默认使用
const。 - 确实需要重新赋值时使用
let。 - 新代码通常不再使用
var。
4.4 暂时性死区
let 和 const 在声明语句执行前不能访问:
javascript
console.log(count); // ReferenceError
let count = 10;
从进入作用域到变量完成初始化之间的区域,通常称为"暂时性死区"。
5. 数据类型
JavaScript 数据类型分为原始类型和对象类型。
5.1 原始类型
JavaScript 有以下原始类型:
| 类型 | 示例 |
|---|---|
undefined |
undefined |
null |
null |
boolean |
true、false |
number |
10、3.14、NaN |
bigint |
9007199254740993n |
string |
"Hello" |
symbol |
Symbol("id") |
5.2 undefined
变量已经声明但尚未赋值时,其值通常是 undefined:
javascript
let result;
console.log(result); // undefined
console.log(typeof result); // "undefined"
5.3 null
null 通常表示开发者明确指定"没有值":
javascript
const selectedUser = null;
需要注意:
javascript
console.log(typeof null); // "object"
这是 JavaScript 长期保留的历史行为。判断 null 应直接比较:
javascript
if (selectedUser === null) {
console.log("当前没有选中用户");
}
5.4 boolean
布尔值只有两个:
javascript
const isAdmin = true;
const isDeleted = false;
以下值转换为布尔值时为 false:
false0-00n""nullundefinedNaN
其他值通常都为 true,包括空数组和空对象:
javascript
console.log(Boolean([])); // true
console.log(Boolean({})); // true
console.log(Boolean("false")); // true
5.5 number
JavaScript 的普通数字使用 number 类型:
javascript
const age = 18;
const price = 99.9;
const temperature = -10;
常见特殊值:
javascript
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(Number("hello")); // NaN
判断 NaN 推荐使用:
javascript
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false
console.log(Number.isNaN(Number("x"))); // true
JavaScript 浮点数可能存在精度问题:
javascript
console.log(0.1 + 0.2); // 0.30000000000000004
金额计算通常应该:
- 使用整数最小货币单位,例如"分"。
- 或使用经过验证的高精度十进制库。
- 不要直接依赖二进制浮点数完成严格财务结算。
javascript
const priceInCents = 1990;
const quantity = 3;
const totalInCents = priceInCents * quantity;
console.log(totalInCents / 100); // 59.7
5.6 bigint
bigint 用于表示超出安全整数范围的整数:
javascript
const largeNumber = 9007199254740993n;
console.log(typeof largeNumber); // "bigint"
bigint 不能直接与 number 混合运算:
javascript
const result = 10n + 20n;
console.log(result); // 30n
下面的代码会报错:
javascript
10n + 20;
5.7 string
字符串可以使用单引号、双引号或模板字符串:
javascript
const firstName = "张";
const lastName = '三';
const fullName = `${firstName}${lastName}`;
console.log(fullName);
字符串是不可变的:
javascript
const text = "JavaScript";
text[0] = "T";
console.log(text); // JavaScript
5.8 symbol
每次调用 Symbol() 都会创建一个唯一值:
javascript
const id1 = Symbol("id");
const id2 = Symbol("id");
console.log(id1 === id2); // false
Symbol 可以用作对象属性键:
javascript
const internalId = Symbol("internalId");
const user = {
name: "张三",
[internalId]: 1001
};
console.log(user[internalId]);
5.9 对象类型
对象可以保存多个属性:
javascript
const user = {
name: "张三",
age: 20,
skills: ["JavaScript", "HTML", "CSS"]
};
数组、函数、日期、正则表达式、Map 和 Set 本质上都属于对象体系:
javascript
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function () {}); // "function"
判断数组应使用:
javascript
console.log(Array.isArray([])); // true
5.10 值传递与引用共享
原始值赋值后互不影响:
javascript
let a = 10;
let b = a;
b = 20;
console.log(a); // 10
console.log(b); // 20
对象变量保存的是对对象的引用:
javascript
const user1 = {
name: "张三"
};
const user2 = user1;
user2.name = "李四";
console.log(user1.name); // 李四
浅拷贝对象:
javascript
const source = {
name: "张三",
address: {
city: "上海"
}
};
const copy = { ...source };
copy.name = "李四";
copy.address.city = "北京";
console.log(source.name); // 张三
console.log(source.address.city); // 北京
展开语法只执行浅拷贝。适合的数据可以使用 structuredClone() 深拷贝:
javascript
const cloned = structuredClone(source);
cloned.address.city = "深圳";
console.log(source.address.city); // 北京
6. 类型转换与相等比较
6.1 显式类型转换
转换为数字:
javascript
console.log(Number("123")); // 123
console.log(Number("")); // 0
console.log(Number("12px")); // NaN
console.log(parseInt("12px", 10)); // 12
console.log(parseFloat("3.14kg")); // 3.14
转换为字符串:
javascript
console.log(String(123)); // "123"
console.log((123).toString()); // "123"
转换为布尔值:
javascript
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean("0")); // true
6.2 隐式类型转换
JavaScript 在某些运算中会自动转换类型:
javascript
console.log("5" + 1); // "51"
console.log("5" - 1); // 4
console.log(true + 1); // 2
+ 同时具有数字加法和字符串拼接两种含义,因此特别容易引起问题。
推荐在数据边界进行显式转换:
javascript
const input = "5";
const value = Number(input);
if (!Number.isFinite(value)) {
throw new Error("输入必须是有效数字");
}
console.log(value + 1);
6.3 == 与 ===
== 在比较前可能进行隐式类型转换:
javascript
console.log(0 == false); // true
console.log("" == false); // true
console.log("1" == 1); // true
=== 不进行这种类型转换:
javascript
console.log(0 === false); // false
console.log("1" === 1); // false
现代项目通常优先使用:
===!==
6.4 Object.is()
Object.is() 与 === 大部分情况下相同,但对 NaN 和正负零的处理不同:
javascript
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false
7. 运算符与流程控制
7.1 算术运算符
javascript
const a = 10;
const b = 3;
console.log(a + b); // 13
console.log(a - b); // 7
console.log(a * b); // 30
console.log(a / b); // 3.333...
console.log(a % b); // 1
console.log(a ** b); // 1000
7.2 逻辑运算符
javascript
const isLogin = true;
const isAdmin = false;
console.log(isLogin && isAdmin); // false
console.log(isLogin || isAdmin); // true
console.log(!isLogin); // false
&& 和 || 返回的不一定是布尔值:
javascript
const user = {
name: "张三"
};
const name = user && user.name;
console.log(name); // 张三
7.3 空值合并运算符
?? 只在左侧为 null 或 undefined 时使用默认值:
javascript
const count = 0;
console.log(count || 10); // 10
console.log(count ?? 10); // 0
如果 0、空字符串或 false 是有效业务值,通常应该使用 ??。
7.4 if 语句
javascript
const score = 85;
if (score >= 90) {
console.log("优秀");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
7.5 三元运算符
javascript
const age = 20;
const message = age >= 18 ? "成年人" : "未成年人";
console.log(message);
三元运算符适合简单表达式,不适合嵌套大量业务逻辑。
7.6 switch 语句
javascript
const status = "paid";
switch (status) {
case "pending":
console.log("待支付");
break;
case "paid":
console.log("已支付");
break;
case "closed":
console.log("已关闭");
break;
default:
console.log("未知状态");
}
7.7 循环
普通 for 循环:
javascript
for (let index = 0; index < 3; index += 1) {
console.log(index);
}
for...of 遍历可迭代对象的值:
javascript
const languages = ["JavaScript", "Java", "Python"];
for (const language of languages) {
console.log(language);
}
for...in 遍历对象的可枚举属性键:
javascript
const user = {
name: "张三",
age: 20
};
for (const key in user) {
if (Object.hasOwn(user, key)) {
console.log(key, user[key]);
}
}
遍历数组通常使用 for...of,不要把 for...in 当作数组值循环。
8. 函数
函数用于封装可复用逻辑。
8.1 函数声明
javascript
function add(a, b) {
return a + b;
}
console.log(add(10, 20));
8.2 函数表达式
javascript
const subtract = function (a, b) {
return a - b;
};
console.log(subtract(20, 10));
8.3 箭头函数
javascript
const multiply = (a, b) => {
return a * b;
};
const square = value => value * value;
console.log(multiply(3, 4));
console.log(square(5));
箭头函数适合:
- 简短回调
- 数组处理
- 不需要独立
this的函数
箭头函数没有自己的:
thisargumentsprototype- 构造调用能力
因此不能使用 new 调用箭头函数。
8.4 默认参数
javascript
function greet(name = "访客") {
return `你好,${name}`;
}
console.log(greet());
console.log(greet("张三"));
8.5 剩余参数
javascript
function sum(...numbers) {
return numbers.reduce((total, number) => total + number, 0);
}
console.log(sum(1, 2, 3, 4));
8.6 回调函数
函数可以作为参数传递:
javascript
function calculate(a, b, operation) {
return operation(a, b);
}
const result = calculate(10, 20, (x, y) => x + y);
console.log(result);
8.7 高阶函数
接收函数或者返回函数的函数称为高阶函数:
javascript
function createMultiplier(factor) {
return value => value * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30
8.8 参数校验
JavaScript 不会自动限制参数类型:
javascript
function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("a 和 b 必须是数字");
}
if (b === 0) {
throw new RangeError("除数不能为 0");
}
return a / b;
}
9. 作用域与闭包
9.1 作用域
常见作用域包括:
- 全局作用域
- 模块作用域
- 函数作用域
- 块级作用域
javascript
const globalValue = "global";
function outer() {
const functionValue = "function";
if (true) {
const blockValue = "block";
console.log(globalValue);
console.log(functionValue);
console.log(blockValue);
}
}
内部作用域可以访问外部作用域,外部作用域不能直接访问内部变量。
9.2 词法作用域
函数能够访问哪些变量,主要由函数定义的位置决定:
javascript
const name = "全局变量";
function outer() {
const name = "外层变量";
function inner() {
console.log(name);
}
return inner;
}
const execute = outer();
execute(); // 外层变量
9.3 闭包
闭包可以理解为"函数与其定义时词法环境的组合"。
javascript
function createCounter() {
let count = 0;
return {
increment() {
count += 1;
},
decrement() {
count -= 1;
},
getValue() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getValue()); // 1
即使 createCounter() 已经执行结束,返回的方法仍然可以访问 count。
闭包常用于:
- 数据封装
- 创建工厂函数
- 缓存
- 防抖与节流
- 事件处理
- 模块私有状态
闭包也可能延长变量生命周期。如果闭包长期持有大型对象,就可能增加内存占用。
10. 对象与原型
10.1 创建对象
javascript
const user = {
id: 1001,
name: "张三",
age: 20,
greet() {
console.log(`你好,我是${this.name}`);
}
};
user.greet();
10.2 访问属性
javascript
console.log(user.name);
console.log(user["age"]);
const propertyName = "name";
console.log(user[propertyName]);
属性名动态计算时,应使用方括号。
10.3 新增、修改与删除属性
javascript
const product = {
name: "键盘"
};
product.price = 299;
product.name = "机械键盘";
delete product.price;
10.4 判断自有属性
javascript
const user = {
name: "张三"
};
console.log(Object.hasOwn(user, "name")); // true
console.log(Object.hasOwn(user, "toString")); // false
10.5 遍历对象
javascript
const user = {
name: "张三",
age: 20
};
console.log(Object.keys(user));
console.log(Object.values(user));
console.log(Object.entries(user));
for (const [key, value] of Object.entries(user)) {
console.log(key, value);
}
10.6 原型链
JavaScript 对象可以通过原型继承其他对象的属性和方法:
javascript
const animal = {
speak() {
console.log(`${this.name} 发出了声音`);
}
};
const dog = Object.create(animal);
dog.name = "旺财";
dog.speak();
当访问 dog.speak 时,JavaScript 会:
- 检查
dog自身是否存在speak。 - 如果不存在,继续查找
dog的原型。 - 沿原型链继续向上查找。
- 到达
null后仍未找到,则返回undefined。
10.7 不要随意修改内置原型
不推荐这样做:
javascript
Array.prototype.first = function () {
return this[0];
};
修改全局内置原型可能造成:
- 命名冲突
- 枚举异常
- 第三方库兼容问题
- 难以排查的全局副作用
更安全的方式是编写普通函数:
javascript
function first(array) {
return array[0];
}
11. 数组与集合
11.1 创建数组
javascript
const numbers = [1, 2, 3, 4];
const empty = [];
11.2 常用操作
javascript
const languages = ["JavaScript", "Java"];
languages.push("Python");
languages.unshift("Go");
const last = languages.pop();
const first = languages.shift();
console.log(first);
console.log(last);
console.log(languages);
11.3 map
map() 将每个元素转换为新元素:
javascript
const numbers = [1, 2, 3];
const squares = numbers.map(number => number ** 2);
console.log(squares); // [1, 4, 9]
11.4 filter
javascript
const users = [
{ name: "张三", active: true },
{ name: "李四", active: false },
{ name: "王五", active: true }
];
const activeUsers = users.filter(user => user.active);
console.log(activeUsers);
11.5 find 与 findIndex
javascript
const users = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
const user = users.find(item => item.id === 2);
const index = users.findIndex(item => item.id === 2);
console.log(user);
console.log(index);
11.6 some 与 every
javascript
const scores = [80, 90, 59];
console.log(scores.some(score => score < 60)); // true
console.log(scores.every(score => score >= 60)); // false
11.7 reduce
javascript
const orders = [
{ amount: 100 },
{ amount: 200 },
{ amount: 300 }
];
const total = orders.reduce(
(sum, order) => sum + order.amount,
0
);
console.log(total); // 600
使用 reduce() 时应提供清晰的初始值。
11.8 sort
sort() 默认按照字符串顺序排序,并且会修改原数组:
javascript
const numbers = [10, 2, 30];
numbers.sort();
console.log(numbers); // [10, 2, 30]
数字排序应传入比较函数:
javascript
const numbers = [10, 2, 30];
numbers.sort((a, b) => a - b);
console.log(numbers); // [2, 10, 30]
不希望修改原数组时:
javascript
const original = [3, 1, 2];
const sorted = [...original].sort((a, b) => a - b);
console.log(original); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
支持较新数组方法的环境也可以使用 toSorted():
javascript
const sorted = original.toSorted((a, b) => a - b);
在生产项目中使用较新 API 前,应检查目标运行环境兼容性。
11.9 Set
Set 用于保存不重复的值:
javascript
const values = new Set([1, 2, 2, 3]);
values.add(4);
values.delete(1);
console.log(values.has(2)); // true
console.log([...values]); // [2, 3, 4]
数组去重:
javascript
const numbers = [1, 1, 2, 3, 3];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers);
11.10 Map
Map 的键可以是任意类型:
javascript
const user = {
id: 1
};
const cache = new Map();
cache.set(user, {
lastLoginTime: Date.now()
});
console.log(cache.get(user));
console.log(cache.has(user));
对象通常适合表示固定结构的数据,Map 更适合频繁增删和动态键值映射。
11.11 WeakMap 与 WeakSet
WeakMap 的键必须是对象,并且不会阻止键对象被垃圾回收:
javascript
const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this, {
name
});
}
getName() {
return privateData.get(this).name;
}
}
WeakMap 和 WeakSet 不可枚举,适合保存与对象生命周期关联的附加数据。
12. 字符串、日期、正则表达式与 JSON
12.1 字符串常用方法
javascript
const text = " JavaScript Tutorial ";
console.log(text.trim());
console.log(text.toLowerCase());
console.log(text.toUpperCase());
console.log(text.includes("Script"));
console.log(text.startsWith(" Java"));
console.log(text.replace("Tutorial", "Guide"));
分割与拼接:
javascript
const value = "JavaScript,Java,Python";
const languages = value.split(",");
console.log(languages);
console.log(languages.join(" | "));
12.2 Unicode 注意事项
JavaScript 字符串的 length 统计的是 UTF-16 代码单元,不一定等于用户看到的字符数:
javascript
const emoji = "😀";
console.log(emoji.length); // 2
console.log([...emoji].length); // 1
复杂 Emoji、组合字符和自然语言分词可能需要 Intl 相关 API 处理。
12.3 日期
javascript
const now = new Date();
console.log(now.toISOString());
console.log(now.getFullYear());
console.log(now.getMonth() + 1);
console.log(now.getDate());
注意:
getMonth()返回0到11。- ISO 时间中的
Z表示 UTC。 - 不带时区的日期字符串可能存在解析差异。
- 前后端应明确时间戳单位、时区和展示规则。
格式化日期可以使用:
javascript
const formatter = new Intl.DateTimeFormat("zh-CN", {
dateStyle: "long",
timeStyle: "medium",
timeZone: "Asia/Shanghai"
});
console.log(formatter.format(new Date()));
12.4 正则表达式
验证简单格式:
javascript
const phonePattern = /^1\d{10}$/;
console.log(phonePattern.test("13800138000"));
提取内容:
javascript
const text = "订单号:A1001,金额:299";
const match = text.match(/订单号:([A-Z]\d+)/);
console.log(match?.[1]);
替换内容:
javascript
const result = "hello javascript".replace(
/javascript/i,
"JavaScript"
);
console.log(result);
正则表达式适合模式匹配,但不应使用过于复杂的正则解析完整 HTML 或复杂编程语言语法。
12.5 JSON
对象转换为 JSON:
javascript
const user = {
id: 1,
name: "张三"
};
const json = JSON.stringify(user);
console.log(json);
JSON 转换为对象:
javascript
const parsed = JSON.parse(json);
console.log(parsed.name);
需要注意:
- JSON 不支持函数。
- JSON 不支持
undefined。 - JSON 不支持循环引用。
Date序列化后通常变成字符串。BigInt不能直接使用默认JSON.stringify()序列化。- 外部 JSON 数据仍然是不可信输入,解析后应验证结构。
13. 解构、展开语法与可选链
13.1 数组解构
javascript
const coordinates = [120.1, 30.2];
const [longitude, latitude] = coordinates;
console.log(longitude);
console.log(latitude);
交换变量:
javascript
let a = 10;
let b = 20;
[a, b] = [b, a];
console.log(a, b);
13.2 对象解构
javascript
const user = {
id: 1,
name: "张三",
age: 20
};
const {
name,
age,
id: userId
} = user;
console.log(name);
console.log(age);
console.log(userId);
默认值:
javascript
const config = {
host: "localhost"
};
const {
host,
port = 8080
} = config;
默认值只在属性值为 undefined 时生效,不会替换 null。
13.3 展开语法
复制数组:
javascript
const source = [1, 2, 3];
const copy = [...source];
合并数组:
javascript
const frontend = ["HTML", "CSS"];
const programming = ["JavaScript", "TypeScript"];
const skills = [...frontend, ...programming];
合并对象:
javascript
const defaultConfig = {
timeout: 5000,
retry: 2
};
const customConfig = {
timeout: 10000
};
const finalConfig = {
...defaultConfig,
...customConfig
};
console.log(finalConfig);
后面的同名属性会覆盖前面的属性。
13.4 可选链
javascript
const user = {
profile: {
address: {
city: "上海"
}
}
};
console.log(user.profile?.address?.city);
console.log(user.company?.name); // undefined
调用可能不存在的方法:
javascript
const result = user.onLogin?.();
13.5 空值合并
javascript
const settings = {
pageSize: 0
};
const pageSize = settings.pageSize ?? 20;
console.log(pageSize); // 0
14. this 的工作机制
this 的值通常由函数如何被调用决定,而不是简单由函数写在哪里决定。
14.1 方法调用
javascript
const user = {
name: "张三",
greet() {
console.log(this.name);
}
};
user.greet(); // 张三
此时 this 指向点号左侧的 user。
14.2 普通函数调用
在严格模式或 ES Module 中:
javascript
function showThis() {
console.log(this);
}
showThis(); // undefined
14.3 显式指定 this
javascript
function greet(message) {
console.log(`${message},${this.name}`);
}
const user = {
name: "张三"
};
greet.call(user, "你好");
greet.apply(user, ["早上好"]);
const boundGreet = greet.bind(user);
boundGreet("欢迎");
区别:
call():立即调用,参数逐个传递。apply():立即调用,参数通过数组传递。bind():返回绑定后的新函数。
14.4 构造调用
javascript
function User(name) {
this.name = name;
}
const user = new User("张三");
console.log(user.name);
使用 new 时,this 指向新创建的对象。
14.5 箭头函数中的 this
箭头函数不会创建自己的 this,而是使用外层作用域的 this:
javascript
const user = {
name: "张三",
greetLater() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
user.greetLater();
不要把需要动态 this 的对象方法随意写成箭头函数:
javascript
const user = {
name: "张三",
greet: () => {
console.log(this.name);
}
};
上面的 this 通常不会指向 user。
14.6 方法被单独取出
javascript
const user = {
name: "张三",
greet() {
console.log(this.name);
}
};
const greet = user.greet;
greet();
单独调用 greet() 时,调用者信息已经丢失。可以使用:
javascript
const greet = user.greet.bind(user);
greet();
15. 类与继承
JavaScript 的 class 是建立在原型机制之上的语法。
15.1 定义类
javascript
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `你好,我是${this.name}`;
}
}
const user = new User("张三", 20);
console.log(user.greet());
15.2 私有字段
javascript
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount <= 0) {
throw new RangeError("充值金额必须大于 0");
}
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance());
类外部不能直接访问 #balance。
15.3 getter 与 setter
javascript
class Temperature {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (!Number.isFinite(value)) {
throw new TypeError("温度必须是有效数字");
}
this.#celsius = value;
}
}
15.4 静态方法
javascript
class MathUtils {
static sum(a, b) {
return a + b;
}
}
console.log(MathUtils.sum(10, 20));
静态方法属于类本身,不属于实例。
15.5 继承
javascript
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} 发出了声音`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${this.name} 汪汪叫`;
}
}
const dog = new Dog("旺财", "柴犬");
console.log(dog.speak());
继承适合明确的"是一个"关系。复杂业务中,组合通常比多层继承更容易维护。
16. 迭代器与生成器
16.1 可迭代对象
数组、字符串、Set 和 Map 都是可迭代对象,可以使用 for...of:
javascript
for (const character of "JavaScript") {
console.log(character);
}
一个对象实现 [Symbol.iterator]() 后,也可以参与迭代:
javascript
const range = {
start: 1,
end: 3,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
}
};
console.log([...range]); // [1, 2, 3]
16.2 生成器
生成器函数使用 function* 声明,通过 yield 逐步产生值:
javascript
function* createRange(start, end) {
for (let value = start; value <= end; value += 1) {
yield value;
}
}
for (const value of createRange(1, 3)) {
console.log(value);
}
生成器适合:
- 惰性序列
- 分页数据
- 状态机
- 自定义迭代逻辑
- 避免一次性创建巨大数组
17. 异常处理
17.1 try、catch 与 finally
javascript
function parseConfig(json) {
try {
return JSON.parse(json);
} catch (error) {
console.error("配置解析失败:", error);
return null;
} finally {
console.log("解析流程结束");
}
}
finally 无论是否发生异常通常都会执行,适合释放资源或恢复状态。
17.2 抛出异常
javascript
function withdraw(balance, amount) {
if (!Number.isFinite(amount) || amount <= 0) {
throw new TypeError("提现金额必须是大于 0 的有效数字");
}
if (amount > balance) {
throw new RangeError("账户余额不足");
}
return balance - amount;
}
推荐抛出 Error 或其子类,不要直接抛出字符串:
javascript
throw new Error("操作失败");
17.3 自定义异常
javascript
class BusinessError extends Error {
constructor(code, message, details = undefined) {
super(message);
this.name = "BusinessError";
this.code = code;
this.details = details;
}
}
function createOrder(stock) {
if (stock <= 0) {
throw new BusinessError(
"OUT_OF_STOCK",
"商品库存不足",
{ stock }
);
}
}
17.4 不要吞掉异常
不推荐:
javascript
try {
riskyOperation();
} catch {
// 什么也不做
}
至少应该记录、转换、恢复或继续抛出:
javascript
try {
riskyOperation();
} catch (error) {
console.error("执行失败", error);
throw error;
}
18. JavaScript 异步模型与事件循环
18.1 为什么需要异步
以下操作通常需要等待:
- 网络请求
- 文件读写
- 数据库访问
- 定时任务
- 用户输入
- 浏览器事件
如果等待期间阻塞主线程,页面可能无法响应用户操作。
JavaScript 通过宿主环境、回调队列、Promise 和事件循环协调异步操作。
18.2 调用栈
同步代码按照调用顺序进入调用栈:
javascript
function first() {
second();
}
function second() {
third();
}
function third() {
console.log("执行 third");
}
first();
调用关系:
text
first()
└── second()
└── third()
函数执行完成后,从调用栈依次退出。
18.3 任务与微任务
浏览器中的异步调度通常会涉及:
- 任务:定时器、用户事件、消息等。
- 微任务 :Promise 回调、
queueMicrotask()等。
示例:
javascript
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise");
});
queueMicrotask(() => {
console.log("microtask");
});
console.log("end");
典型输出:
text
start
end
promise
microtask
timeout
基本过程可以简化理解为:
- 执行当前同步代码。
- 当前调用栈清空。
- 执行已经排队的微任务。
- 进入下一轮事件循环,选择可执行任务。
- 浏览器在合适时机进行渲染。
事件循环的具体细节由宿主环境决定。浏览器与 Node.js 的调度阶段并不完全相同。
18.4 setTimeout 并不保证精确时间
javascript
setTimeout(() => {
console.log("至少等待约 1000 毫秒后进入可执行队列");
}, 1000);
1000 毫秒表示达到最小等待条件后进入调度,并不保证回调在第 1000 毫秒精确执行。
如果主线程正忙,回调会继续等待。
18.5 避免阻塞主线程
javascript
const start = Date.now();
while (Date.now() - start < 5000) {
// 阻塞主线程约 5 秒
}
这段代码可能导致:
- 页面无法点击
- 动画卡顿
- 输入无响应
- 定时器延迟
- 渲染停止
CPU 密集型任务可以考虑:
- 拆分为小任务
- 优化算法
- 使用 Web Worker
- 在 Node.js 中使用 Worker Threads
- 将重计算交给服务端或专用服务
19. Promise 与 async/await
19.1 Promise 的状态
Promise 有三种状态:
pending:等待中fulfilled:已成功rejected:已失败
状态一旦从 pending 变为成功或失败,就不会再次改变。
19.2 创建 Promise
javascript
function delay(milliseconds) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
delay(1000).then(() => {
console.log("等待完成");
});
19.3 then、catch 与 finally
javascript
fetchUser()
.then(user => {
console.log(user);
return user.id;
})
.then(userId => {
console.log("用户 ID:", userId);
})
.catch(error => {
console.error("操作失败:", error);
})
.finally(() => {
console.log("流程结束");
});
每次调用 then()、catch() 或 finally(),通常都会返回一个新的 Promise,因此可以形成链式调用。
19.4 async/await
async/await 是建立在 Promise 之上的语法:
javascript
async function loadUser() {
try {
const user = await fetchUser();
console.log(user);
return user;
} catch (error) {
console.error("加载用户失败:", error);
throw error;
}
}
async 函数总是返回 Promise:
javascript
async function getValue() {
return 100;
}
getValue().then(value => {
console.log(value);
});
19.5 串行与并行
串行执行:
javascript
const user = await loadUser();
const orders = await loadOrders();
如果两个操作没有依赖关系,可以并行启动:
javascript
const [user, orders] = await Promise.all([
loadUser(),
loadOrders()
]);
错误地先后 await 独立任务,会增加总等待时间。
19.6 Promise.all
全部成功时返回结果数组,只要有一个失败就会整体拒绝:
javascript
const [users, products, orders] = await Promise.all([
loadUsers(),
loadProducts(),
loadOrders()
]);
Promise.all() 失败后,其他已经启动的任务不会自动取消。
19.7 Promise.allSettled
需要收集每个任务结果时:
javascript
const results = await Promise.allSettled([
loadUsers(),
loadProducts(),
loadOrders()
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("成功:", result.value);
} else {
console.error("失败:", result.reason);
}
}
19.8 Promise.race 与 Promise.any
Promise.race() 返回第一个完成的结果,无论成功还是失败:
javascript
const result = await Promise.race([
requestFromServerA(),
requestFromServerB()
]);
Promise.any() 返回第一个成功的结果。如果全部失败,则拒绝并产生 AggregateError:
javascript
const result = await Promise.any([
requestFromServerA(),
requestFromServerB()
]);
19.9 不要在 forEach 中直接 await
错误示例:
javascript
users.forEach(async user => {
await saveUser(user);
});
console.log("保存结束");
forEach() 不会等待异步回调完成。
串行处理:
javascript
for (const user of users) {
await saveUser(user);
}
并行处理:
javascript
await Promise.all(
users.map(user => saveUser(user))
);
并发量较大时,应使用并发限制,而不是一次启动成千上万个请求。
20. 浏览器 DOM 操作
DOM 是浏览器将 HTML 文档表示为对象树的方式。
20.1 查询元素
javascript
const title = document.querySelector("#title");
const buttons = document.querySelectorAll(".button");
querySelector() 返回第一个匹配元素,找不到时返回 null。
javascript
const title = document.querySelector("#title");
if (!title) {
throw new Error("没有找到标题元素");
}
20.2 修改文本和属性
javascript
const title = document.querySelector("#title");
title.textContent = "新的标题";
title.classList.add("active");
title.setAttribute("data-status", "ready");
操作表单元素:
javascript
const input = document.querySelector("#username");
console.log(input.value);
input.value = "张三";
20.3 创建元素
javascript
const list = document.querySelector("#user-list");
const item = document.createElement("li");
item.textContent = "张三";
item.dataset.userId = "1001";
list.append(item);
20.4 批量创建元素
javascript
const users = ["张三", "李四", "王五"];
const fragment = document.createDocumentFragment();
for (const user of users) {
const item = document.createElement("li");
item.textContent = user;
fragment.append(item);
}
document.querySelector("#user-list").append(fragment);
使用 DocumentFragment 可以减少反复插入 DOM 的次数。
20.5 textContent 与 innerHTML
处理不可信文本时,优先使用:
javascript
element.textContent = userInput;
下面的方式可能产生 XSS 风险:
javascript
element.innerHTML = userInput;
如果业务必须渲染 HTML,应使用经过验证的 HTML 清理方案,并配置适当的内容安全策略。
21. 浏览器事件系统
21.1 监听事件
javascript
const button = document.querySelector("#submit-button");
button.addEventListener("click", event => {
console.log("按钮被点击", event);
});
21.2 阻止默认行为
javascript
const form = document.querySelector("#login-form");
form.addEventListener("submit", event => {
event.preventDefault();
console.log("执行自定义提交逻辑");
});
21.3 事件传播
浏览器事件通常经历:
- 捕获阶段
- 到达目标
- 冒泡阶段
javascript
document.body.addEventListener("click", () => {
console.log("body");
});
document.querySelector("#button").addEventListener("click", () => {
console.log("button");
});
默认监听器通常在冒泡阶段执行。
21.4 事件委托
如果列表有大量动态元素,可以只在父元素上注册监听器:
javascript
const list = document.querySelector("#user-list");
list.addEventListener("click", event => {
if (!(event.target instanceof Element)) {
return;
}
const button = event.target.closest("button[data-action]");
if (!button || !list.contains(button)) {
return;
}
const action = button.dataset.action;
const userId = button.dataset.userId;
console.log(action, userId);
});
事件委托的优点:
- 减少事件监听器数量
- 动态新增元素无需重新绑定
- 统一管理交互逻辑
21.5 移除监听器
要移除监听器,需要保留同一个函数引用:
javascript
function handleClick() {
console.log("clicked");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);
也可以借助 AbortController 管理监听器生命周期:
javascript
const controller = new AbortController();
button.addEventListener(
"click",
() => {
console.log("clicked");
},
{ signal: controller.signal }
);
controller.abort();
22. Fetch 网络请求
22.1 GET 请求
javascript
async function loadUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`请求失败,HTTP 状态码:${response.status}`);
}
return response.json();
}
需要特别注意:fetch() 遇到 404、500 等 HTTP 错误状态时,通常不会自动拒绝 Promise,因此必须检查 response.ok 或 response.status。
22.2 POST 请求
javascript
async function createUser(user) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error(`创建用户失败:${response.status}`);
}
return response.json();
}
22.3 请求超时与取消
javascript
async function fetchJson(url, timeout = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} finally {
clearTimeout(timer);
}
}
调用:
javascript
try {
const data = await fetchJson("/api/data", 3000);
console.log(data);
} catch (error) {
if (error.name === "AbortError") {
console.error("请求已取消或超时");
} else {
console.error("请求失败", error);
}
}
22.4 并非所有响应都是 JSON
javascript
const response = await fetch("/api/report");
const contentType = response.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
const data = await response.json();
console.log(data);
} else {
const text = await response.text();
console.log(text);
}
22.5 CORS
跨源请求受到浏览器同源策略和 CORS 机制约束。
CORS 是否允许访问主要由目标服务器的响应头决定。前端代码不能通过添加一个普通请求头来"关闭 CORS",mode: "no-cors" 也不是读取任意跨域响应的通用解决方案。
23. 浏览器存储
23.1 localStorage
javascript
const settings = {
theme: "dark",
pageSize: 20
};
localStorage.setItem(
"settings",
JSON.stringify(settings)
);
const saved = localStorage.getItem("settings");
const parsed = saved ? JSON.parse(saved) : null;
console.log(parsed);
特点:
- 按源隔离。
- 数据以字符串形式保存。
- API 是同步的。
- 通常会长期保留,直到用户或程序删除。
- 容量与具体浏览器策略有关。
解析外部存储数据时应处理异常:
javascript
function loadSettings() {
try {
const raw = localStorage.getItem("settings");
if (!raw) {
return {};
}
const value = JSON.parse(raw);
return value && typeof value === "object"
? value
: {};
} catch (error) {
console.error("读取设置失败", error);
return {};
}
}
23.2 sessionStorage
sessionStorage 的 API 与 localStorage 类似,但生命周期通常与当前页面会话相关:
javascript
sessionStorage.setItem("currentStep", "2");
23.3 Cookie
Cookie 可以由浏览器随 HTTP 请求发送到服务器。
安全相关 Cookie 通常应由服务端设置:
HttpOnlySecure- 合适的
SameSite - 合理的
Path和有效期
不要把 Cookie、localStorage 或 sessionStorage 当作可信数据库。
23.4 IndexedDB
当浏览器需要保存较多结构化数据时,可以考虑 IndexedDB。
适用场景包括:
- 离线数据
- 大量结构化记录
- 二进制数据
- 客户端缓存
- 支持索引和事务的本地存储
24. JavaScript 模块化
24.1 导出模块
math.js:
javascript
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.1415926;
24.2 导入模块
main.js:
javascript
import {
add,
subtract,
PI
} from "./math.js";
console.log(add(10, 20));
console.log(subtract(20, 10));
console.log(PI);
24.3 默认导出
logger.js:
javascript
export default function log(message) {
console.log(`[LOG] ${message}`);
}
导入:
javascript
import log from "./logger.js";
log("应用启动");
一个模块只能有一个默认导出,但可以有多个命名导出。
24.4 重命名导入
javascript
import {
add as calculateTotal
} from "./math.js";
console.log(calculateTotal(10, 20));
24.5 动态导入
javascript
async function loadEditor() {
const module = await import("./editor.js");
module.openEditor();
}
动态导入适合:
- 路由懒加载
- 按需加载大型功能
- 降低初始资源体积
- 根据运行条件加载模块
24.6 浏览器模块
html
<script type="module" src="./main.js"></script>
浏览器模块具有以下特点:
- 默认使用严格模式。
- 支持
import和export。 - 默认延迟执行。
- 相对导入通常需要写明文件扩展名。
- 使用独立模块作用域,不会把顶层变量自动挂载到
window。 - 本地开发通常需要通过 HTTP 服务访问。
24.7 Node.js ES Module
package.json:
json
{
"type": "module"
}
之后可以在 .js 文件中使用:
javascript
import fs from "node:fs/promises";
const content = await fs.readFile("./data.txt", "utf8");
console.log(content);
Node.js 同时存在 ES Module 和 CommonJS 两种模块体系。新项目应明确选择模块类型,避免依赖模糊推断。
CommonJS 示例:
javascript
const fs = require("node:fs");
module.exports = {
readFile: fs.readFile
};
不要在不了解互操作规则的情况下随意混用 import、export、require() 和 module.exports。
25. Node.js 与 npm
25.1 创建项目
bash
mkdir javascript-demo
cd javascript-demo
npm init -y
生成的 package.json 用于描述:
- 项目名称
- 项目版本
- 模块类型
- 执行脚本
- 生产依赖
- 开发依赖
25.2 package.json
json
{
"name": "javascript-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/main.js",
"test": "node --test"
}
}
运行脚本:
bash
npm run start
npm test
25.3 安装依赖
生产依赖:
bash
npm install package-name
开发依赖:
bash
npm install --save-dev package-name
不要盲目安装依赖。一个简单函数能解决的问题,未必需要引入大型第三方包。
25.4 锁文件
npm 通常会生成 package-lock.json。
锁文件用于记录精确依赖解析结果,有助于:
- 提高安装结果一致性
- 追踪间接依赖
- 在 CI 中复现依赖树
应用项目通常应将锁文件提交到版本控制。
25.5 环境变量
javascript
const port = Number(process.env.PORT ?? 3000);
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("缺少 DATABASE_URL 环境变量");
}
环境变量的值通常是字符串或 undefined,使用前应完成类型转换和校验。
不要把密码、令牌或私钥直接写入源代码。
26. 函数式编程思想
26.1 纯函数
纯函数具有两个主要特点:
- 相同输入产生相同输出。
- 不修改函数外部状态。
javascript
function addTax(price, taxRate) {
return price * (1 + taxRate);
}
非纯函数示例:
javascript
let total = 0;
function addToTotal(value) {
total += value;
}
26.2 不可变更新
不直接修改原对象:
javascript
const user = {
id: 1,
name: "张三",
age: 20
};
const updatedUser = {
...user,
age: 21
};
console.log(user.age); // 20
console.log(updatedUser.age); // 21
更新数组:
javascript
const users = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
const updatedUsers = users.map(user =>
user.id === 2
? { ...user, name: "王五" }
: user
);
26.3 函数组合
javascript
const trim = value => value.trim();
const toLowerCase = value => value.toLowerCase();
const removeSpaces = value => value.replaceAll(" ", "-");
const normalizeSlug = value =>
removeSpaces(toLowerCase(trim(value)));
console.log(normalizeSlug(" Hello JavaScript "));
26.4 声明式处理
命令式:
javascript
const result = [];
for (const number of numbers) {
if (number % 2 === 0) {
result.push(number * 2);
}
}
声明式:
javascript
const result = numbers
.filter(number => number % 2 === 0)
.map(number => number * 2);
声明式代码不一定永远更快,但通常更容易表达数据转换意图。
27. 性能优化
27.1 先测量,再优化
不要只凭感觉优化。可以使用:
- 浏览器 Performance 面板
- Network 面板
- Memory 面板
- Lighthouse
- Node.js 性能分析工具
performance.now()console.time()
javascript
console.time("calculation");
performCalculation();
console.timeEnd("calculation");
27.2 选择合适的数据结构
频繁按键查询时,Map 或普通对象通常比反复遍历数组更合适:
javascript
const users = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
const usersById = new Map(
users.map(user => [user.id, user])
);
console.log(usersById.get(2));
27.3 减少重复计算
javascript
function memoize(fn) {
const cache = new Map();
return value => {
if (cache.has(value)) {
return cache.get(value);
}
const result = fn(value);
cache.set(value, result);
return result;
};
}
缓存需要控制:
- 缓存键
- 数据有效期
- 最大容量
- 失效策略
- 对象引用与内存占用
27.4 防抖
防抖适合搜索输入、窗口尺寸变化等高频事件:
javascript
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const handleSearch = debounce(keyword => {
console.log("搜索:", keyword);
}, 300);
27.5 节流
javascript
function throttle(fn, interval) {
let lastExecutionTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastExecutionTime < interval) {
return;
}
lastExecutionTime = now;
fn.apply(this, args);
};
}
节流适合滚动、拖动和指针移动等需要定期响应的场景。
27.6 减少 DOM 开销
建议:
- 批量创建和插入节点。
- 避免在循环中重复查询相同元素。
- 使用事件委托。
- 避免不必要的布局读取与写入交错。
- 大列表使用分页、虚拟列表或增量渲染。
27.7 代码拆分
通过动态导入按需加载:
javascript
button.addEventListener("click", async () => {
const { openChart } = await import("./chart.js");
openChart();
});
27.8 异步不等于多线程
把 CPU 密集型函数标记为 async,不会自动让它进入另一个线程:
javascript
async function heavyCalculation() {
// 这里仍可能阻塞当前 JavaScript 执行线程
}
真正需要并行计算时,应考虑 Worker。
28. JavaScript 安全实践
28.1 防止 XSS
危险示例:
javascript
result.innerHTML = userInput;
安全显示纯文本:
javascript
result.textContent = userInput;
还应结合:
- 服务端输出编码
- HTML 清理
- 内容安全策略
- 安全的模板系统
- Cookie 安全属性
28.2 不要在前端保存真正的秘密
前端代码最终会发送到用户设备,用户可以查看:
- JavaScript 源代码
- 网络请求
- 浏览器存储
- 构建产物
- Source Map
因此不能把以下内容作为秘密硬编码在前端:
- 数据库密码
- 服务端私钥
- 管理员令牌
- 无访问限制的第三方密钥
28.3 避免 eval
javascript
eval(userInput);
eval() 会执行字符串代码,可能带来代码注入和维护问题。
动态数据应通过 JSON 解析:
javascript
const data = JSON.parse(jsonText);
28.4 验证外部输入
任何外部数据都应该被视为不可信:
- 表单输入
- URL 参数
- localStorage
- Cookie
- API 响应
- WebSocket 消息
- 第三方 SDK 返回值
javascript
function validateUser(value) {
if (!value || typeof value !== "object") {
throw new TypeError("用户数据必须是对象");
}
if (typeof value.id !== "number") {
throw new TypeError("用户 ID 必须是数字");
}
if (typeof value.name !== "string") {
throw new TypeError("用户名必须是字符串");
}
return value;
}
前端校验主要改善用户体验,不能代替服务端权限校验和数据校验。
28.5 防范原型污染
合并不可信对象时,应限制允许的字段:
javascript
function updateProfile(input) {
const profile = {};
if (typeof input.name === "string") {
profile.name = input.name;
}
if (typeof input.avatar === "string") {
profile.avatar = input.avatar;
}
return profile;
}
不要无条件把外部对象的所有属性复制到内部配置或全局对象。
28.6 依赖安全
建议:
- 控制依赖数量。
- 检查包的维护状态。
- 提交锁文件。
- 审查自动执行的安装脚本。
- 定期扫描已知漏洞。
- 不要只根据包名相似度安装依赖。
- 升级前查看变更说明并运行测试。
29. 调试、测试与工程化
29.1 console
javascript
console.log("普通信息");
console.info("提示信息");
console.warn("警告信息");
console.error("错误信息");
表格展示:
javascript
console.table([
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
]);
断言:
javascript
console.assert(
total >= 0,
"总金额不应该小于 0"
);
29.2 debugger
javascript
function calculateTotal(items) {
debugger;
return items.reduce(
(total, item) => total + item.price,
0
);
}
浏览器开发者工具打开时,代码会在 debugger 处暂停。
29.3 错误堆栈
javascript
try {
performOperation();
} catch (error) {
console.error(error.message);
console.error(error.stack);
}
排查错误时,应优先记录:
- 错误类型
- 错误消息
- 调用堆栈
- 输入参数摘要
- 业务 ID
- 请求 ID
- 运行环境
记录日志时应避免泄露密码、令牌和个人敏感信息。
29.4 使用 Node.js 内置测试能力
src/sum.js:
javascript
export function sum(a, b) {
return a + b;
}
test/sum.test.js:
javascript
import test from "node:test";
import assert from "node:assert/strict";
import { sum } from "../src/sum.js";
test("sum 应返回两个数字之和", () => {
assert.equal(sum(10, 20), 30);
});
test("sum 应正确处理负数", () => {
assert.equal(sum(-10, 5), -5);
});
运行:
bash
node --test
29.5 测试应覆盖什么
常见测试层次:
- 单元测试
- 集成测试
- API 测试
- 端到端测试
- 性能测试
- 安全测试
- 人工验收
重点场景包括:
- 正常输入
- 空值
- 非法类型
- 边界值
- 重复提交
- 并发操作
- 网络超时
- 接口失败
- 权限不足
- 异步竞争
- 数据恢复
29.6 ESLint 与格式化工具
静态检查工具可以帮助发现:
- 未使用变量
- 可疑类型转换
- 未处理 Promise
- 不一致代码风格
- 潜在逻辑问题
代码格式化工具则用于统一缩进、换行和标点风格。
工具不能代替代码审查和测试,但可以减少机械性错误。
29.7 JSDoc
javascript
/**
* 计算订单总金额。
*
* @param {Array<{price: number, quantity: number}>} items 订单项
* @returns {number} 总金额
*/
function calculateTotal(items) {
return items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
}
29.8 TypeScript
TypeScript 在 JavaScript 基础上增加了静态类型系统。
它适合:
- 中大型项目
- 多人协作项目
- 公共组件库
- 复杂数据模型
- 需要大规模重构的系统
但学习 TypeScript 不能替代 JavaScript 基础。作用域、闭包、原型、this、事件循环和 Promise 等机制仍然属于 JavaScript 本身。
30. 完整实战:待办事项应用
下面使用原生 HTML、CSS 和 JavaScript 实现一个待办事项应用。
它包含:
- 新增任务
- 完成任务
- 删除任务
- 本地持久化
- 事件委托
- 输入校验
- 安全的 DOM 更新
项目结构:
text
todo-app/
├── index.html
├── style.css
└── main.js
30.1 index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript 待办事项</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<main class="todo-app">
<h1>待办事项</h1>
<form id="todo-form" class="todo-form">
<label for="todo-input">任务名称</label>
<div class="todo-form__controls">
<input
id="todo-input"
name="title"
type="text"
maxlength="80"
autocomplete="off"
placeholder="请输入任务名称"
required
>
<button type="submit">添加</button>
</div>
</form>
<p id="todo-summary" aria-live="polite"></p>
<ul id="todo-list" class="todo-list"></ul>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
30.2 style.css
css
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
padding: 40px 16px;
color: #1f2937;
background: #f3f4f6;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
button,
input {
font: inherit;
}
.todo-app {
width: min(100%, 640px);
margin: 0 auto;
padding: 32px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 16px 40px rgb(15 23 42 / 10%);
}
.todo-app h1 {
margin-top: 0;
}
.todo-form label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
.todo-form__controls {
display: flex;
gap: 12px;
}
.todo-form input {
flex: 1;
min-width: 0;
padding: 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.todo-form button,
.todo-item__delete {
padding: 10px 16px;
border: 0;
border-radius: 8px;
color: #ffffff;
background: #2563eb;
cursor: pointer;
}
.todo-list {
display: grid;
gap: 10px;
margin: 24px 0 0;
padding: 0;
list-style: none;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.todo-item__title {
flex: 1;
overflow-wrap: anywhere;
}
.todo-item--completed .todo-item__title {
color: #64748b;
text-decoration: line-through;
}
.todo-item__delete {
background: #dc2626;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid rgb(37 99 235 / 30%);
outline-offset: 2px;
}
30.3 main.js
javascript
const STORAGE_KEY = "javascript-blog.todos.v1";
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
const list = document.querySelector("#todo-list");
const summary = document.querySelector("#todo-summary");
if (!form || !input || !list || !summary) {
throw new Error("页面缺少待办事项应用所需元素");
}
let todos = loadTodos();
render();
form.addEventListener("submit", event => {
event.preventDefault();
const title = input.value.trim();
if (!title) {
input.focus();
return;
}
todos = [
{
id: createId(),
title,
completed: false,
createdAt: new Date().toISOString()
},
...todos
];
saveTodos();
render();
form.reset();
input.focus();
});
list.addEventListener("change", event => {
const target = event.target;
if (
!(target instanceof HTMLInputElement) ||
target.dataset.action !== "toggle"
) {
return;
}
const item = target.closest("li[data-id]");
if (!item) {
return;
}
const todoId = item.dataset.id;
todos = todos.map(todo =>
todo.id === todoId
? {
...todo,
completed: target.checked
}
: todo
);
saveTodos();
render();
});
list.addEventListener("click", event => {
if (!(event.target instanceof Element)) {
return;
}
const button = event.target.closest(
"button[data-action='delete']"
);
if (!button || !list.contains(button)) {
return;
}
const item = button.closest("li[data-id]");
if (!item) {
return;
}
const todoId = item.dataset.id;
todos = todos.filter(todo => todo.id !== todoId);
saveTodos();
render();
});
function createId() {
if (globalThis.crypto?.randomUUID) {
return crypto.randomUUID();
}
return [
Date.now(),
Math.random().toString(16).slice(2)
].join("-");
}
function loadTodos() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return [];
}
const value = JSON.parse(raw);
if (!Array.isArray(value)) {
return [];
}
return value.filter(isValidTodo);
} catch (error) {
console.error("读取待办事项失败", error);
return [];
}
}
function isValidTodo(todo) {
return (
todo &&
typeof todo === "object" &&
typeof todo.id === "string" &&
typeof todo.title === "string" &&
typeof todo.completed === "boolean"
);
}
function saveTodos() {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(todos)
);
} catch (error) {
console.error("保存待办事项失败", error);
}
}
function render() {
const fragment = document.createDocumentFragment();
for (const todo of todos) {
const item = document.createElement("li");
item.className = "todo-item";
item.dataset.id = todo.id;
if (todo.completed) {
item.classList.add("todo-item--completed");
}
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.completed;
checkbox.dataset.action = "toggle";
checkbox.setAttribute(
"aria-label",
`将"${todo.title}"标记为${todo.completed ? "未完成" : "已完成"}`
);
const title = document.createElement("span");
title.className = "todo-item__title";
title.textContent = todo.title;
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "todo-item__delete";
deleteButton.dataset.action = "delete";
deleteButton.textContent = "删除";
deleteButton.setAttribute(
"aria-label",
`删除任务"${todo.title}"`
);
item.append(
checkbox,
title,
deleteButton
);
fragment.append(item);
}
list.replaceChildren(fragment);
const completedCount = todos.filter(
todo => todo.completed
).length;
summary.textContent =
`共 ${todos.length} 项,已完成 ${completedCount} 项`;
}
30.4 项目知识点分析
这个项目使用了:
const与let- 数组和对象
- 模板字符串
- 函数
- ES Module
- DOM 查询
- DOM 节点创建
textContent- 表单事件
- 事件委托
map()与filter()- 对象展开语法
- localStorage
- JSON 序列化
- 异常处理
- 数据结构校验
- 可访问性属性
任务标题通过 textContent 写入页面,没有直接拼接到 innerHTML 中,因此降低了注入恶意 HTML 的风险。
31. 常见错误与避坑指南
31.1 浮点数精度
javascript
console.log(0.1 + 0.2 === 0.3); // false
不要直接使用普通浮点数进行严格财务计算。
31.2 使用 == 导致隐式转换
javascript
console.log("" == 0); // true
优先使用严格相等:
javascript
console.log("" === 0); // false
31.3 把空数组和空对象当作 false
javascript
if ([]) {
console.log("空数组也是 truthy");
}
if ({}) {
console.log("空对象也是 truthy");
}
判断数组是否为空:
javascript
if (array.length === 0) {
console.log("数组为空");
}
31.4 展开语法不是深拷贝
javascript
const source = {
profile: {
city: "上海"
}
};
const copy = {
...source
};
copy.profile.city = "北京";
console.log(source.profile.city); // 北京
31.5 Array.fill 共享对象
javascript
const rows = Array(3).fill({ count: 0 });
rows[0].count = 10;
console.log(rows);
三个位置引用的是同一个对象。
正确创建独立对象:
javascript
const rows = Array.from(
{ length: 3 },
() => ({ count: 0 })
);
31.6 sort 修改原数组
javascript
const source = [3, 1, 2];
const sorted = source.sort((a, b) => a - b);
console.log(source); // 已经被修改
可以先复制:
javascript
const sorted = [...source].sort((a, b) => a - b);
31.7 forEach 不等待异步函数
javascript
items.forEach(async item => {
await processItem(item);
});
需要等待时使用 for...of 或 Promise.all()。
31.8 return 后错误换行
javascript
function createUser() {
return
{
name: "张三"
};
}
由于自动分号插入,实际相当于:
javascript
function createUser() {
return;
}
应该写成:
javascript
function createUser() {
return {
name: "张三"
};
}
31.9 丢失 this
javascript
const user = {
name: "张三",
greet() {
console.log(this.name);
}
};
setTimeout(user.greet, 100);
可以显式绑定:
javascript
setTimeout(user.greet.bind(user), 100);
31.10 fetch 不自动处理 HTTP 错误
javascript
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
不要认为进入 then() 就一定代表业务请求成功。
31.11 忘记处理 Promise
javascript
saveData();
如果 saveData() 返回 Promise,调用方应根据需要等待或明确处理:
javascript
await saveData();
或者:
javascript
saveData().catch(error => {
console.error(error);
});
31.12 把异步误认为并行
async/await 主要改善异步流程表达方式,不会自动把 CPU 计算放到另一个线程。
31.13 忽略日期和时区
以下概念必须明确区分:
- UTC 时间
- 用户本地时间
- 服务端时区
- 数据库存储时区
- 秒级时间戳
- 毫秒级时间戳
- 只表示日期、不表示时刻的业务字段
32. JavaScript 学习路线
第一阶段:语言基础
学习:
- 变量
- 数据类型
- 运算符
- 条件判断
- 循环
- 函数
- 数组
- 对象
练习:
- 计算器
- 成绩统计
- 字符串处理
- 数组去重
- 数据分组
第二阶段:核心机制
学习:
- 作用域
- 闭包
this- 原型链
- 类
- 模块
- 错误处理
练习:
- 计数器
- 缓存函数
- 数据模型
- 模块化工具库
第三阶段:异步编程
学习:
- 回调函数
- 事件循环
- Promise
async/await- Fetch
- 超时与取消
- 并发控制
练习:
- 请求用户列表
- 并行加载数据
- 请求重试
- 搜索防抖
- 分页加载
第四阶段:浏览器开发
学习:
- HTML
- CSS
- DOM
- 事件
- 表单
- Storage
- 浏览器调试工具
- 安全基础
练习:
- 待办事项
- 记账应用
- Markdown 编辑器
- 天气查询页面
第五阶段:Node.js 与工程化
学习:
- npm
- package.json
- ES Module
- 文件系统
- HTTP 服务
- 环境变量
- 日志
- 测试
- 代码规范
- Git
第六阶段:框架
具备 JavaScript 基础后,再选择:
- React
- Vue
- Angular
- Svelte
- Node.js 服务端框架
不要只会调用框架 API,而不了解 JavaScript 的作用域、闭包、对象、异步和模块机制。
第七阶段:TypeScript
掌握 JavaScript 后学习:
- 基础类型
- 接口
- 类型别名
- 泛型
- 联合类型
- 类型收窄
- 工具类型
- 声明文件
33. 常见问题
33.1 JavaScript 是单线程的吗
更准确地说,一个 JavaScript Agent 通常一次执行一个任务中的 JavaScript 代码,但宿主环境可以使用其他线程处理网络、计时器、文件 I/O、渲染或 Worker。
因此,"JavaScript 单线程"不等于整个浏览器或 Node.js 进程只有一个线程。
33.2 JavaScript 和 Java 有什么关系
二者是不同的语言。
主要区别包括:
- 类型系统不同
- 对象模型不同
- 运行环境不同
- 并发模型不同
- 生态系统不同
名称相似不代表语言机制相同。
33.3 应该使用 var、let 还是 const
推荐:
- 默认使用
const。 - 需要重新赋值时使用
let。 - 新代码通常不使用
var。
33.4 什么时候学习 TypeScript
建议先掌握:
- 函数
- 对象
- 数组
- 作用域
- 闭包
- 原型
- 类
- Promise
- 模块化
然后再学习 TypeScript。这样才能区分语言运行时问题和类型检查问题。
33.5 什么时候开始学习框架
当你能够使用原生 JavaScript 完成以下内容时,就可以开始学习框架:
- 操作 DOM
- 处理表单
- 发起网络请求
- 管理简单状态
- 编写模块
- 处理异步错误
- 使用 npm
- 调试代码
33.6 Promise 和 async/await 有什么关系
async/await 是基于 Promise 的语法。
它不会替代 Promise,而是让 Promise 链在很多场景下更接近同步代码的阅读方式。
33.7 学习 JavaScript 最重要的内容是什么
除了基础语法,最重要的核心机制包括:
- 作用域
- 闭包
- 原型链
this- 值与对象引用
- 事件循环
- Promise
- 模块化
- 运行环境差异
理解这些内容后,学习前端框架和 Node.js 都会更加顺利。
34. 总结
JavaScript 已经从一种网页脚本语言发展为通用编程语言。它既可以处理网页交互,也可以开发服务端程序、桌面应用、命令行工具和自动化系统。
学习 JavaScript 不应该只停留在语法层面,还需要建立以下完整认识:
- JavaScript 核心语言与宿主 API 是不同层次。
- 默认使用
const,需要重新赋值时使用let。 - 了解原始值和对象引用的区别。
- 使用严格相等,减少隐式类型转换。
- 理解作用域、闭包、原型和
this。 - 理解调用栈、任务、微任务和事件循环。
- 使用 Promise 和
async/await管理异步流程。 - 网络请求必须处理 HTTP 状态、超时和异常。
- 外部输入、浏览器存储和接口响应都需要校验。
- 通过模块、测试、代码检查和持续集成提高项目质量。
- 在性能优化前先进行测量。
- 不要把前端校验当作服务端安全措施。
真正掌握 JavaScript 的关键不是记住所有 API,而是理解它的运行模型,并通过持续编写、调试和重构真实项目形成稳定的编程能力。