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
11
printf("第一步\n");
sleep(1); // 等待1秒
printf("第二步\n");
sleep(1);
printf("第三步\n");
// 输出:
// 第一步
// (等待1秒)
// 第二步
// (等待1秒)
// 第三步

在C/C++中,代码按顺序执行,每一步都会等待上一步完成。

JavaScript中的"意外"行为

假设我们要从服务器获取用户信息,然后显示用户名。如果你按照C/C++的思维写代码:

1
2
3
4
5
6
7
8
console.log('开始获取用户信息');

// 从服务器获取用户数据(看起来像普通函数调用)
const response = fetch('https://api.example.com/user/1');
const user = response.json(); // 解析JSON数据
console.log('用户名:', user.name);

console.log('继续执行其他代码');

你期望的输出

1
2
3
4
开始获取用户信息
(等待网络请求完成)
用户名:张三
继续执行其他代码

实际输出

1
2
3
开始获取用户信息
继续执行其他代码
(然后报错:Cannot read property 'name' of undefined)

为什么会这样?

fetch()函数看起来像普通的函数调用,但它实际上是异步操作。当JavaScript执行到fetch()时,它不会等待网络请求完成,而是立即返回一个Promise对象,然后继续执行后面的代码。此时response还不是实际的响应数据,所以response.json()会失败。

正确的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('开始获取用户信息');

// fetch返回Promise,不会阻塞后续代码
const responsePromise = fetch('https://api.example.com/user/1');
console.log('responsePromise的类型:', typeof responsePromise); // object (Promise)

// 此时responsePromise还不是实际数据,需要等待
console.log('继续执行其他代码'); // 这行会立即执行

// 实际输出:
// 开始获取用户信息
// responsePromise的类型:object
// 继续执行其他代码

为什么会出现这种情况?

JavaScript是单线程的,但它使用事件循环机制处理异步操作。当遇到fetch()setTimeout()等异步函数时,JavaScript不会等待它们完成,而是继续执行后面的代码。异步操作完成后,会通过回调函数或Promise处理结果。

问题:如果我们想在获取数据后再执行某些操作,该怎么办?

这就是为什么需要Promise和async/await的原因——它们帮助我们以更清晰的方式处理异步操作。


1.1.2 传统方式的问题:回调地狱

1
2
3
4
5
6
7
8
9
10
11
// 煮饭
cookRice(function() {
// 炒菜
cookDish(function() {
// 洗碗
washDishes(function() {
console.log('全部完成!');
// 如果还有更多步骤,代码会越来越深,像"地狱"一样
});
});
});

现代方式(async/await):

1
2
3
4
5
6
async 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
8
const 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
3
fetchData.then(function(result) {
console.log(result); // 输出:数据获取成功!
});


2.2.2 catch()方法 - 处理失败情况

语法promise.catch(失败回调函数)

实际例子

1
2
3
4
5
6
7
fetchData
.then(function(result) {
console.log(result); // 成功时执行
})
.catch(function(error) {
console.log(error); // 失败时执行
});


2.2.3 完整示例

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
// 模拟获取用户信息
function getUserInfo(userId) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (userId === 1) {
resolve({
id: 1,
name: '张三',
email: 'zhangsan@example.com'
});
} else {
reject('用户不存在');
}
}, 1000);
});
}

// 使用Promise
getUserInfo(1)
.then(function(user) {
console.log('用户信息:', user);
// 输出:用户信息:{id: 1, name: '张三', email: 'zhangsan@example.com'}
})
.catch(function(error) {
console.log('错误:', error);
});

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
4
async 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
9
async function 函数名() {
try {
const result = await Promise对象;
// 成功时执行
} catch (error) {
// 失败时执行
console.log('错误:', error);
}
}

实际例子

1
2
3
4
5
6
7
8
async 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
8
const 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
4
const 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
10
const 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
8
const 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
9
const 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
9
const 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
10
const 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
9
const 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
9
const 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
12
const numbers = [1, 2, 3, 4, 5];

// 打印每个数字
numbers.forEach(num => {
console.log(num);
});
// 输出:
// 1
// 2
// 3
// 4
// 5

需要注意的是,forEach方法不返回新数组,仅用于遍历执行副作用操作。如果需要基于原数组生成新数组,应使用map方法。


4.3 数组方法组合使用

实际例子:处理用户数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const users = [
{ name: '张三', age: 20, score: 85 },
{ name: '李四', age: 25, score: 92 },
{ name: '王五', age: 18, score: 78 },
{ name: '赵六', age: 22, score: 95 }
];

// 1. 筛选出年龄大于等于20的用户
// 2. 提取他们的名字
// 3. 转换为大写

const result = users
.filter(user => user.age >= 20) // 筛选
.map(user => user.name) // 提取名字
.map(name => name.toUpperCase()); // 转大写

console.log(result); // ['张三', '李四', '赵六']

5. 解构赋值:让数据提取更简洁

5.1 什么是解构赋值?

5.1.1 解构赋值的概念

解构赋值是ES6引入的语法特性,允许从数组或对象中提取值,并将它们赋值给不同的变量。这种语法使代码更简洁,减少了重复的赋值操作。


5.1.2 数组解构

语法const [变量1, 变量2, ...] = 数组

实际例子

1
2
3
4
5
6
7
8
9
10
11
12
const 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
16
const 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
12
let 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
46
import { 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
44
import { 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 代码解析

核心思路

  1. 使用async/await处理异步操作(loadTodos
  2. 使用数组方法处理数据(filtermap
  3. 使用解构赋值提取数据(getStats
  4. 使用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开发的基础。建议通过实际项目练习,逐步熟悉这些特性的应用场景和最佳实践。