05.现代JavaScript
1. 为什么需要现代JavaScript?
1.1 从"回调地狱"到"优雅异步"
在第四章中,我们学习了JavaScript的基础语法、DOM操作、事件监听和表单验证。然而,当需要处理异步操作(如网络请求、文件读取)时,传统的回调函数方式会导致代码结构复杂、难以维护。
1.1.1 JavaScript的异步特性:与C/C++的不同
如果你有C/C++的编程经验,可能会习惯这样的代码执行顺序:
C/C++中的执行顺序: 1
2
3
4
5
6
7
8
9
10
11printf("第一步\n");
sleep(1); // 等待1秒
printf("第二步\n");
sleep(1);
printf("第三步\n");
// 输出:
// 第一步
// (等待1秒)
// 第二步
// (等待1秒)
// 第三步
在C/C++中,代码按顺序执行,每一步都会等待上一步完成。
JavaScript中的"意外"行为:
假设我们要从服务器获取用户信息,然后显示用户名。如果你按照C/C++的思维写代码:
1 | console.log('开始获取用户信息'); |
你期望的输出: 1
2
3
4开始获取用户信息
(等待网络请求完成)
用户名:张三
继续执行其他代码
实际输出: 1
2
3开始获取用户信息
继续执行其他代码
(然后报错:Cannot read property 'name' of undefined)
为什么会这样?
fetch()函数看起来像普通的函数调用,但它实际上是异步操作。当JavaScript执行到fetch()时,它不会等待网络请求完成,而是立即返回一个Promise对象,然后继续执行后面的代码。此时response还不是实际的响应数据,所以response.json()会失败。
正确的理解:
1 | console.log('开始获取用户信息'); |
为什么会出现这种情况?
JavaScript是单线程的,但它使用事件循环机制处理异步操作。当遇到fetch()、setTimeout()等异步函数时,JavaScript不会等待它们完成,而是继续执行后面的代码。异步操作完成后,会通过回调函数或Promise处理结果。
问题:如果我们想在获取数据后再执行某些操作,该怎么办?
这就是为什么需要Promise和async/await的原因——它们帮助我们以更清晰的方式处理异步操作。
1.1.2 传统方式的问题:回调地狱
1 | // 煮饭 |
现代方式(async/await): 1
2
3
4
5
6async function makeDinner() {
await cookRice(); // 等待煮饭完成
await cookDish(); // 等待炒菜完成
await washDishes(); // 等待洗碗完成
console.log('全部完成!');
}
现代JavaScript通过Promise和async/await语法,使异步代码的编写方式更接近同步代码,提高了代码的可读性和可维护性。
1.2 现代JavaScript的核心特性
| 特性 | 作用 | 为什么重要 |
|---|---|---|
| Promise | 处理异步操作 | 解决回调地狱问题 |
| async/await | 让异步代码更简洁 | 代码更易读、易维护 |
| 数组方法 | 处理数组数据 | 代码更简洁、功能更强大 |
| 解构赋值 | 提取数据 | 代码更简洁 |
| 模块化 | 组织代码 | 代码更易管理 |
ES6(2015年)及后续版本的发布标志着JavaScript从简单的脚本语言演进为功能完整的现代编程语言,引入了类、模块、Promise等企业级开发所需的核心特性。
2. Promise:异步编程的基础
2.1 什么是Promise?
2.1.1 Promise的概念
Promise是JavaScript中用于处理异步操作的对象,它表示一个异步操作的最终完成(或失败)及其结果值。
Promise有三种状态:
- pending(待定):初始状态,既不是成功也不是失败
- fulfilled(已兑现):操作成功完成
- rejected(已拒绝):操作失败
Promise的状态只能从pending转换为fulfilled或rejected,且状态一旦改变就不会再变。
2.1.2 Promise的定义
Promise = 表示一个异步操作的最终结果(成功或失败)
基本语法: 1
2
3
4
5
6
7
8const promise = new Promise(function(resolve, reject) {
// 异步操作
if (成功) {
resolve(结果); // 成功时调用
} else {
reject(错误); // 失败时调用
}
});
实际例子: 1
2
3
4
5
6
7
8
9
10
11// 模拟获取数据(异步操作)
const fetchData = new Promise(function(resolve, reject) {
setTimeout(function() {
const success = true; // 模拟成功/失败
if (success) {
resolve('数据获取成功!');
} else {
reject('数据获取失败!');
}
}, 1000); // 1秒后执行
});
2.2 如何使用Promise?
2.2.1 then()方法 - 处理成功情况
语法:promise.then(成功回调函数)
实际例子: 1
2
3fetchData.then(function(result) {
console.log(result); // 输出:数据获取成功!
});
2.2.2 catch()方法 - 处理失败情况
语法:promise.catch(失败回调函数)
实际例子: 1
2
3
4
5
6
7fetchData
.then(function(result) {
console.log(result); // 成功时执行
})
.catch(function(error) {
console.log(error); // 失败时执行
});
2.2.3 完整示例
1 | // 模拟获取用户信息 |
2.3 Promise链式调用
Promise的优势:可以链式调用,避免回调地狱
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 传统方式(回调地狱)
getUserInfo(1, function(user) {
getUserPosts(user.id, function(posts) {
getPostComments(posts[0].id, function(comments) {
console.log(comments); // 代码越来越深,难以阅读
});
});
});
// Promise方式(链式调用)
getUserInfo(1)
.then(function(user) {
return getUserPosts(user.id);
})
.then(function(posts) {
return getPostComments(posts[0].id);
})
.then(function(comments) {
console.log(comments); // 代码扁平,易于阅读
})
.catch(function(error) {
console.log('错误:', error);
});
Promise的链式调用特性使异步代码从嵌套结构转变为扁平结构,显著提升了代码的可读性和可维护性。
3. async/await:让异步代码更优雅
3.1 什么是async/await?
3.1.1 async/await的概念
async/await是ES2017引入的语法糖,基于Promise实现,使异步代码的编写和阅读更接近同步代码。
async:用于声明一个异步函数,该函数返回一个Promise对象await:只能在async函数中使用,用于等待Promise完成并获取其结果
3.1.2 async/await的定义
async/await = 让异步代码看起来像同步代码的语法糖
基本语法: 1
2
3
4async function 函数名() {
const result = await Promise对象;
// 使用result
}
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12// 使用Promise
getUserInfo(1)
.then(function(user) {
console.log(user);
});
// 使用async/await(更简洁)
async function displayUser() {
const user = await getUserInfo(1);
console.log(user);
}
displayUser();
3.2 async/await的优势
| 优势 | 说明 | 实际效果 |
|---|---|---|
| 代码更简洁 | 不需要写.then()和.catch() |
代码行数减少50% |
| 更易读 | 看起来像同步代码 | 更容易理解 |
| 错误处理 | 使用try/catch处理错误 |
错误处理更统一 |
3.3 错误处理:try/catch
语法: 1
2
3
4
5
6
7
8
9async function 函数名() {
try {
const result = await Promise对象;
// 成功时执行
} catch (error) {
// 失败时执行
console.log('错误:', error);
}
}
实际例子: 1
2
3
4
5
6
7
8async function displayUser() {
try {
const user = await getUserInfo(1);
console.log('用户信息:', user);
} catch (error) {
console.log('获取用户信息失败:', error);
}
}
3.4 实战案例:使用async/await获取数据
需求:从API获取用户信息,然后获取该用户的文章列表
代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 模拟API函数
function getUserInfo(userId) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ id: userId, name: '张三' });
}, 1000);
});
}
function getUserPosts(userId) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve([
{ id: 1, title: '文章1' },
{ id: 2, title: '文章2' }
]);
}, 1000);
});
}
// 使用async/await
async function loadUserData(userId) {
try {
console.log('正在获取用户信息...');
const user = await getUserInfo(userId);
console.log('用户信息:', user);
console.log('正在获取文章列表...');
const posts = await getUserPosts(user.id);
console.log('文章列表:', posts);
return { user, posts };
} catch (error) {
console.log('错误:', error);
}
}
// 调用函数
loadUserData(1);
输出: 1
2
3
4正在获取用户信息...
用户信息:{id: 1, name: '张三'}
正在获取文章列表...
文章列表:[{id: 1, title: '文章1'}, {id: 2, title: '文章2'}]
async/await使异步代码的编写方式更接近同步代码,但不会阻塞JavaScript事件循环,其他代码仍可正常执行。
4. 数组方法:让数据处理更简单
4.1 为什么需要数组方法?
4.1.1 传统方式的痛点
传统方式(使用for循环): 1
2
3
4
5
6
7
8const numbers = [1, 2, 3, 4, 5];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
console.log(doubled); // [2, 4, 6, 8, 10]
现代方式(使用数组方法): 1
2
3
4const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
数组方法提供了声明式的数据处理方式,相比命令式的for循环,代码更简洁、语义更清晰,且更容易进行函数式编程的组合。
4.2 常用数组方法
4.2.1 map() - 转换数组
作用:对数组中的每个元素执行函数,返回新数组
语法:数组.map(函数)
实际例子: 1
2
3
4
5
6
7
8
9
10const numbers = [1, 2, 3, 4, 5];
// 将每个数字乘以2
const doubled = numbers.map(function(num) {
return num * 2;
});
// 或使用箭头函数
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
更多例子: 1
2
3
4
5
6
7
8const users = [
{ name: '张三', age: 20 },
{ name: '李四', age: 25 }
];
// 提取用户名
const names = users.map(user => user.name);
console.log(names); // ['张三', '李四']
4.2.2 filter() - 过滤数组
作用:根据条件过滤数组,返回新数组
语法:数组.filter(函数)
实际例子: 1
2
3
4
5
6
7
8
9const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 筛选出偶数
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]
// 筛选出大于5的数
const largeNumbers = numbers.filter(num => num > 5);
console.log(largeNumbers); // [6, 7, 8, 9, 10]
更多例子: 1
2
3
4
5
6
7
8
9const users = [
{ name: '张三', age: 20 },
{ name: '李四', age: 25 },
{ name: '王五', age: 18 }
];
// 筛选出年龄大于等于20的用户
const adults = users.filter(user => user.age >= 20);
console.log(adults); // [{name: '张三', age: 20}, {name: '李四', age: 25}]
4.2.3 reduce() - 累积计算
作用:对数组中的每个元素执行函数,累积成一个值
语法:数组.reduce(函数, 初始值)
实际例子: 1
2
3
4
5
6
7
8
9
10const numbers = [1, 2, 3, 4, 5];
// 计算总和
const sum = numbers.reduce(function(acc, num) {
return acc + num;
}, 0);
// 或使用箭头函数
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15
更多例子: 1
2
3
4
5
6
7
8
9const items = [
{ name: '苹果', price: 10 },
{ name: '香蕉', price: 5 },
{ name: '橙子', price: 8 }
];
// 计算总价
const total = items.reduce((acc, item) => acc + item.price, 0);
console.log(total); // 23
4.2.4 find() - 查找元素
作用:查找数组中第一个满足条件的元素
语法:数组.find(函数)
实际例子: 1
2
3
4
5
6
7
8
9const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
];
// 查找id为2的用户
const user = users.find(u => u.id === 2);
console.log(user); // {id: 2, name: '李四'}
4.2.5 forEach() - 遍历数组
作用:遍历数组,对每个元素执行函数(不返回新数组)
语法:数组.forEach(函数)
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12const numbers = [1, 2, 3, 4, 5];
// 打印每个数字
numbers.forEach(num => {
console.log(num);
});
// 输出:
// 1
// 2
// 3
// 4
// 5
需要注意的是,forEach方法不返回新数组,仅用于遍历执行副作用操作。如果需要基于原数组生成新数组,应使用map方法。
4.3 数组方法组合使用
实际例子:处理用户数据
1 | const users = [ |
5. 解构赋值:让数据提取更简洁
5.1 什么是解构赋值?
5.1.1 解构赋值的概念
解构赋值是ES6引入的语法特性,允许从数组或对象中提取值,并将它们赋值给不同的变量。这种语法使代码更简洁,减少了重复的赋值操作。
5.1.2 数组解构
语法:const [变量1, 变量2, ...] = 数组
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12const numbers = [1, 2, 3];
// 传统方式
const first = numbers[0];
const second = numbers[1];
const third = numbers[2];
// 解构赋值(更简洁)
const [first, second, third] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(third); // 3
5.1.3 对象解构
语法:const {属性1, 属性2, ...} = 对象
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const user = {
name: '张三',
age: 20,
email: 'zhangsan@example.com'
};
// 传统方式
const name = user.name;
const age = user.age;
const email = user.email;
// 解构赋值(更简洁)
const { name, age, email } = user;
console.log(name); // 张三
console.log(age); // 20
console.log(email); // zhangsan@example.com
5.2 解构赋值的实际应用
5.2.1 函数参数解构
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13// 传统方式
function displayUser(user) {
console.log(user.name);
console.log(user.age);
}
// 解构赋值(更简洁)
function displayUser({ name, age }) {
console.log(name);
console.log(age);
}
displayUser({ name: '张三', age: 20 });
5.2.2 交换变量
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12let a = 1;
let b = 2;
// 传统方式(需要临时变量)
let temp = a;
a = b;
b = temp;
// 解构赋值(一行搞定)
[a, b] = [b, a];
console.log(a); // 2
console.log(b); // 1
6. 模块化:组织代码的艺术
6.1 为什么需要模块化?
6.1.1 传统方式的痛点
传统方式(所有代码写在一个文件):
1
2
3
4
5
6
7<script>
// 1000行代码都在这里
function function1() { ... }
function function2() { ... }
function function3() { ... }
// ... 代码越来越长,难以维护
</script>
问题:
- 代码太长,难以维护
- 变量名可能冲突
- 无法复用代码
6.1.2 模块化的优势
| 优势 | 说明 | 实际效果 |
|---|---|---|
| 代码组织 | 按功能分成多个文件 | 代码更清晰、易维护 |
| 避免冲突 | 每个模块有独立作用域 | 变量名不会冲突 |
| 代码复用 | 可以在多个地方使用 | 提高开发效率 |
6.2 ES6模块化语法
6.2.1 export - 导出
语法:export 变量/函数/类
实际例子: 1
2
3
4
5
6
7
8
9
10// utils.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14;
6.2.2 import - 导入
语法:import { 变量/函数/类 } from '文件路径'
实际例子: 1
2
3
4
5
6// main.js
import { add, subtract, PI } from './utils.js';
console.log(add(1, 2)); // 3
console.log(subtract(5, 3)); // 2
console.log(PI); // 3.14
6.2.3 默认导出
语法: 1
2
3
4
5// 导出
export default 变量/函数/类;
// 导入
import 名称 from '文件路径';
实际例子: 1
2
3
4
5
6
7
8
9// user.js
export default {
name: '张三',
age: 20
};
// main.js
import user from './user.js';
console.log(user.name); // 张三
7. AI实战:使用AI生成现代JavaScript代码
7.1 任务描述
目标:使用AI工具生成一个使用现代JavaScript特性的待办事项应用
功能要求:
- 使用async/await获取数据
- 使用数组方法处理数据
- 使用解构赋值提取数据
- 使用模块化组织代码
7.2 第一步:设计Prompt
Prompt示例: 1
2
3
4
5
6
7用现代JavaScript创建一个待办事项应用,要求:
1. 使用async/await处理异步操作
2. 使用数组方法(map、filter、reduce)处理数据
3. 使用解构赋值提取数据
4. 使用ES6模块化组织代码
5. 包含添加、删除、完成待办事项功能
6. 代码注释清晰,使用现代JavaScript语法
7.3 AI生成的代码(示例)
utils.js(工具函数模块): 1
2
3
4
5
6
7
8
9
10
11
12
13// 模拟API延迟
export function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 获取待办事项列表
export async function fetchTodos() {
await delay(1000); // 模拟网络延迟
return [
{ id: 1, text: '学习JavaScript', completed: false },
{ id: 2, text: '完成作业', completed: true }
];
}
todoService.js(待办事项服务): 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import { fetchTodos } from './utils.js';
let todos = [];
// 加载待办事项
export async function loadTodos() {
try {
todos = await fetchTodos();
return todos;
} catch (error) {
console.error('加载失败:', error);
return [];
}
}
// 添加待办事项
export function addTodo(text) {
const newTodo = {
id: Date.now(),
text,
completed: false
};
todos.push(newTodo);
return newTodo;
}
// 删除待办事项
export function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
}
// 切换完成状态
export function toggleTodo(id) {
todos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
// 获取统计信息
export function getStats() {
const total = todos.length;
const completed = todos.filter(todo => todo.completed).length;
const pending = total - completed;
return { total, completed, pending };
}
main.js(主文件): 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44import { loadTodos, addTodo, deleteTodo, toggleTodo, getStats } from './todoService.js';
// 初始化
async function init() {
const todos = await loadTodos();
renderTodos(todos);
updateStats();
}
// 渲染待办事项
function renderTodos(todos) {
const list = document.getElementById('todoList');
list.innerHTML = todos.map(todo => `
<li class="${todo.completed ? 'completed' : ''}">
<input type="checkbox" ${todo.completed ? 'checked' : ''}
onchange="toggleTodo(${todo.id})">
<span>${todo.text}</span>
<button onclick="deleteTodo(${todo.id})">删除</button>
</li>
`).join('');
}
// 更新统计信息
function updateStats() {
const { total, completed, pending } = getStats();
document.getElementById('stats').textContent =
`总计:${total},已完成:${completed},待完成:${pending}`;
}
// 添加待办事项
document.getElementById('addBtn').addEventListener('click', () => {
const input = document.getElementById('todoInput');
const text = input.value.trim();
if (text) {
addTodo(text);
input.value = '';
renderTodos(todos);
updateStats();
}
});
// 初始化
init();
7.4 代码解析
核心思路:
- 使用
async/await处理异步操作(loadTodos) - 使用数组方法处理数据(
filter、map) - 使用解构赋值提取数据(
getStats) - 使用ES6模块化组织代码(
import/export)
关键点:
- 所有异步操作使用
async/await - 所有数组操作使用数组方法
- 代码按功能分成多个模块
8. 本节总结
你已经学会了:
✅ Promise和async/await:
- Promise的基本用法
- async/await的语法和优势
- 错误处理(try/catch)
✅ 数组方法:
map()- 转换数组filter()- 过滤数组reduce()- 累积计算find()- 查找元素forEach()- 遍历数组
✅ 解构赋值:
- 数组解构
- 对象解构
- 函数参数解构
✅ 模块化:
export- 导出import- 导入- 默认导出
✅ 实战技能:
- 使用AI生成现代JavaScript代码
- 组织模块化代码
现代JavaScript的这些特性显著提升了代码质量和开发效率。掌握Promise、async/await、数组方法、解构赋值和模块化等核心概念,是进行现代Web开发的基础。建议通过实际项目练习,逐步熟悉这些特性的应用场景和最佳实践。