JavaScript异步编程、迭代器与生成器全解析
1. 异步JavaScript
在JavaScript中,异步操作是常见的,如数据获取。以下是一个简单的异步数据获取示例:
fetchData(/* some url */)
.then((data) => {
/* do something with the data */
})
.catch((error) => {
/* handle error */
});
这里的
fetchData
函数用于获取数据,
then
方法处理成功响应,
catch
方法处理错误。也可以在
fetchData
函数内部处理错误,代码如下:
function fetchData(url) {
fetch(url)
.then((response) => response.json())
.then((data) => {
/* do something with the data */
})
.catch((err) => {
/* error handling code */
});
}
fetchData(/* some url */);
1.1 承诺拒绝转换为履行
Promise.prototype
对象上的每个方法都会返回一个新的承诺。如果不小心,可能会编写将承诺拒绝隐式转换为承诺履行的代码。例如:
function getData(url) {
return Promise.reject(new Error()).catch((err) => {
console.log("inside catch block in getData function");
});
}
getData()
.then((data) => console.log("then block"))
.catch((error) => console.log("catch block"));
预期输出可能是“catch block”,但实际输出是:
"inside catch block in getData function"
"then block"
代码执行流程如下:
1. 调用
getData
函数。
2.
Promise.reject(new Error())
创建一个被拒绝的承诺。
3. 由于承诺被拒绝,
catch
方法的回调函数被调用。
4. 控制台输出“inside catch block in getData function”。
5. 因为
catch
方法的回调函数没有显式返回任何内容,所以隐式返回
undefined
。
6.
catch
方法返回的承诺以其回调函数的返回值(即
undefined
)被履行。
7.
getData
函数将这个已履行的承诺返回给调用代码。
8. 由于
getData
函数返回的承诺以值
undefined
被履行,调用代码中
then
方法的回调被调用,输出“then block”。
要修复这个问题,有两种方法:
-
从
catch
方法的回调函数中抛出错误
:
function getData(url) {
return Promise.reject(new Error()).catch((err) => {
throw err;
});
}
这样会拒绝
catch
方法返回的承诺,调用代码中的
catch
方法回调将被调用。
-
移除
catch
方法调用
:
function getData(url) {
return Promise.reject(new Error());
}
这也会调用调用代码中的
catch
块,因为
getData
函数现在返回
Promise.reject
的结果,而
Promise.reject
会创建一个被拒绝的承诺。
1.2 异步执行器函数
使用
Promise
构造函数创建新承诺时,会传递一个函数给构造函数,这个函数称为执行器函数。执行器函数不应该是异步函数。例如:
const p = new Promise(async (resolve, reject) => {
throw new Error("error");
});
p.catch((e) => console.log(e.message));
由于执行器函数是异步函数,其中抛出的错误不会导致新创建的承诺被拒绝,
catch
方法的回调函数不会被调用。如果执行器函数是同步函数,其中抛出的任何错误都会自动拒绝新创建的承诺。
2. 迭代器和生成器
2.1
for...of
循环
内置对象(如数组)可以使用
for...of
循环进行迭代。与简单的
for
循环相比,
for...of
循环更方便,不需要手动处理索引和循环终止条件。示例如下:
const arr = [1, 2, 3];
for (const num of arr) {
console.log(num);
}
要理解
for...of
循环如何帮助我们迭代数组,需要了解两个概念:可迭代对象和迭代器。
2.2 可迭代对象
可迭代对象是实现了可迭代协议的对象。根据可迭代协议,如果一个对象定义了可被
for...of
循环用于迭代其值的迭代行为,则该对象是可迭代的。对象可以实现由
Symbol.iterator
表示的属性所引用的方法,这个方法返回一个迭代器对象。例如,数组的
Symbol.iterator
方法定义在
Array.prototype
对象中。
2.3 迭代器
迭代器是实现了迭代器协议的对象。根据迭代器协议,一个对象如果实现了名为
next
的方法,该方法接受零个或一个参数,并返回一个具有以下属性的对象,则该对象是迭代器:
-
done
:指示迭代器是否可以产生或返回另一个值。如果可以,其值为
false
,否则为
true
。
-
value
:迭代器对象返回的值。如果
done
属性的值为
true
,则该属性可以省略或其值可以为
undefined
。
迭代器对象示例:
{ value: 45, done: false }
{ value: undefined, done: true }
可以直接使用数组的迭代器来获取数组中的值,代码如下:
const arr = [2, 4, 6, 8, 10];
const arrayIterator = arr[Symbol.iterator]();
let result = arrayIterator.next();
while (!result.done) {
console.log(result.value);
result = arrayIterator.next();
}
内置的迭代器对象为可迭代对象定义了特定的迭代行为。例如,
Map
对象的迭代器使用示例:
const myMap = new Map();
myMap.set("a", 1);
myMap.set("b", 2);
myMap.set("c", 3);
const mapIterator = myMap[Symbol.iterator]();
let result = mapIterator.next();
while (!result.done) {
console.log(result.value);
result = mapIterator.next();
}
可以使用
Map.prototype.values()
获取只返回
Map
中值的迭代器,或使用
Map.prototype.keys()
获取返回
Map
中所有键的迭代器。
2.4 迭代器原型
每个迭代器对象都继承自相应的迭代器原型对象,所有迭代器原型对象都继承自
Iterator.prototype
对象。例如:
const arr = [2, 4, 6, 8, 10];
const arrayIterator = arr[Symbol.iterator]();
const arrayIteratorPrototype = Object.getPrototypeOf(arrayIterator);
console.log(Object.getOwnPropertyNames(arrayIteratorPrototype));
// ["next"]
Iterator.prototype
对象本身是一个可迭代对象,这意味着任何迭代器对象本身都是可迭代的,可以与
for...of
循环一起使用。例如:
const arr = [1, 2, 3];
const arrayIterator = arr[Symbol.iterator]();
for (const num of arrayIterator) {
console.log(num);
}
2.5 创建自定义可迭代对象
了解了可迭代对象和迭代器后,可以创建自定义可迭代对象。例如,创建可迭代的学生对象:
function Student(name, age, id, courses) {
this.name = name;
this.age = age;
this.id = id;
this.courses = courses;
}
Student.prototype[Symbol.iterator] = function () {
const currentStudent = this;
const studentProps = Object.getOwnPropertyNames(currentStudent);
let propIndex = 0;
const studentIterator = {
next: () => {
if (propIndex < studentProps.length) {
const key = studentProps[propIndex];
const value = currentStudent[key];
propIndex++;
const formattedValue = `${key.padStart(7)} => ${value}`;
return {
value: formattedValue,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
return studentIterator;
};
const jack = new Student("Jack", 20, "21A", ["Maths", "Biology", "Physics"]);
for (const val of jack) {
console.log(val);
}
默认情况下,学生迭代器对象不可迭代。可以通过在学生迭代器对象中实现
Symbol.iterator
方法使其可迭代:
Student.prototype[Symbol.iterator] = function () {
const studentIterator = {
next() {
// code omitted to keep code example short
},
[Symbol.iterator]() {
return this;
}
};
return studentIterator;
};
还可以使用
Object.defineProperty
方法将
Symbol.iterator
方法定义为不可枚举:
Object.defineProperty(Student.prototype, Symbol.iterator, {
value: function () {
// copy the code inside the Symbol.iterator method from above
},
configurable: true,
writable: true
});
2.6 生成器
生成器是JavaScript中的特殊函数,可以在执行过程中的不同点暂停执行,然后从暂停点继续执行。生成器函数可以用于生成一系列值,可被
for...of
循环等结构消费。例如,生成0到10之间的奇数:
function* odds() {
for (let i = 1; i <= 10; i += 2) {
yield i;
}
}
for (const num of odds()) {
console.log(num);
}
生成器函数的特点:
-
function*
语法将函数标记为生成器函数。
-
yield
关键字产生一个值,调用生成器函数返回的迭代器对象的
next
方法时,会得到
yield
关键字右侧的值,
yield
关键字也标记了生成器函数暂停的位置。
生成器函数还可以创建无限序列,例如生成随机数:
function* randomNumberGenerator(max) {
while (true) {
yield Math.floor(Math.random() * max);
}
}
const randomNumGen = randomNumberGenerator(10);
for (let i = 0; i < 10; i++) {
console.log(randomNumGen.next().value);
}
生成器函数是惰性求值的,其执行会暂停,直到请求新的值。
2.7 使用生成器实现迭代器
由于生成器函数返回一个迭代器对象,因此可以方便地编写迭代器。可以使用生成器函数重写学生迭代器示例:
function Student(name, age, id, courses) {
this.name = name;
this.age = age;
this.id = id;
this.courses = courses;
}
Student.prototype[Symbol.iterator] = function* () {
const currentStudent = this;
const studentProps = Object.getOwnPropertyNames(currentStudent);
for (let i = 0; i < studentProps.length; i++) {
const key = studentProps[i];
const value = currentStudent[key];
const formattedValue = `${key.padStart(7)} => ${value}`;
yield formattedValue;
}
};
const jack = new Student("Jack", 20, "21A", ["Maths", "Biology", "Physics"]);
for (const val of jack) {
console.log(val);
}
使用生成器函数实现的迭代器代码更简单、简洁、易读,并且生成器函数返回的迭代器对象自动可迭代。
总结
本文详细介绍了JavaScript中的异步编程、迭代器和生成器的相关知识。异步编程中要注意承诺拒绝和履行的转换问题,避免代码出现意外行为。迭代器和生成器为处理序列数据提供了强大的工具,可以方便地创建自定义可迭代对象和生成序列值。通过合理运用这些特性,可以编写出更高效、更具可读性的JavaScript代码。
3. 技术点对比与应用场景分析
3.1 异步编程技术对比
| 技术 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|
普通
Promise
链式调用
| 代码结构清晰,易于理解和维护 | 可能出现回调地狱问题 | 简单的异步操作链,如数据获取和处理 |
解决承诺拒绝转换问题的
Promise
| 避免承诺拒绝隐式转换为履行,确保错误能被正确处理 | 可能增加代码复杂度 | 涉及网络请求等可能失败的异步操作 |
| 异步执行器函数(避免使用异步) | 确保错误能正确导致承诺拒绝 | 需注意执行器函数不能为异步 | 自定义承诺创建场景 |
例如,在一个简单的用户信息获取应用中,使用普通
Promise
链式调用:
function fetchUserInfo() {
return fetch('https://example.com/user')
.then(response => response.json())
.then(data => {
console.log('User info:', data);
return data;
})
.catch(error => {
console.error('Error fetching user info:', error);
});
}
fetchUserInfo();
3.2 迭代器与生成器应用场景对比
| 技术 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|
| 迭代器 | 可自定义迭代行为,直接控制迭代过程 | 代码实现相对复杂 | 自定义对象迭代,如自定义数据结构 |
| 生成器 | 代码简洁,自动实现迭代器协议,支持无限序列 | 理解难度稍大 | 生成序列数据,如生成随机数、斐波那契数列等 |
例如,在一个学生信息管理系统中,使用迭代器自定义学生对象迭代:
function Student(name, age, id, courses) {
this.name = name;
this.age = age;
this.id = id;
this.courses = courses;
}
Student.prototype[Symbol.iterator] = function () {
const currentStudent = this;
const studentProps = Object.getOwnPropertyNames(currentStudent);
let propIndex = 0;
const studentIterator = {
next: () => {
if (propIndex < studentProps.length) {
const key = studentProps[propIndex];
const value = currentStudent[key];
propIndex++;
const formattedValue = `${key.padStart(7)} => ${value}`;
return {
value: formattedValue,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
return studentIterator;
};
const jack = new Student("Jack", 20, "21A", ["Maths", "Biology", "Physics"]);
for (const val of jack) {
console.log(val);
}
而在一个游戏开发中,使用生成器生成随机关卡编号:
function* randomLevelGenerator(max) {
while (true) {
yield Math.floor(Math.random() * max);
}
}
const levelGen = randomLevelGenerator(10);
for (let i = 0; i < 5; i++) {
console.log('Random level:', levelGen.next().value);
}
4. 操作步骤总结
4.1 异步编程错误处理操作步骤
-
识别问题
:检查代码中是否存在承诺拒绝隐式转换为履行的情况,例如在
catch方法中未正确处理错误。 -
选择解决方案
:
-
如果需要在
catch方法中处理错误并继续传递错误,使用throw err。 -
如果不需要在当前函数中处理错误,直接移除
catch方法调用。
-
如果需要在
- 修改代码 :根据选择的解决方案修改代码,确保错误能被正确处理。
4.2 创建自定义可迭代对象操作步骤
-
定义对象构造函数
:如定义
Student构造函数,用于创建对象实例。 -
实现
Symbol.iterator方法 :在对象的原型上实现Symbol.iterator方法,该方法返回一个迭代器对象。 -
定义迭代器对象的
next方法 :在迭代器对象中定义next方法,根据迭代逻辑返回具有value和done属性的对象。 -
可选:使迭代器对象可迭代
:在迭代器对象中实现
Symbol.iterator方法,返回this。 -
可选:使
Symbol.iterator方法不可枚举 :使用Object.defineProperty方法定义Symbol.iterator方法。
4.3 使用生成器实现迭代器操作步骤
-
定义对象构造函数
:如定义
Student构造函数。 -
使用生成器函数实现
Symbol.iterator方法 :在对象的原型上使用function*定义Symbol.iterator方法,使用yield关键字生成迭代值。 -
使用对象进行迭代
:创建对象实例,使用
for...of循环进行迭代。
5. 流程图展示
graph TD;
A[开始] --> B[异步编程];
B --> B1[普通Promise链式调用];
B --> B2[解决承诺拒绝转换问题];
B --> B3[异步执行器函数];
C[迭代器和生成器];
C --> C1[迭代器];
C --> C2[生成器];
B1 --> D[简单异步操作链];
B2 --> E[网络请求等可能失败的操作];
B3 --> F[自定义承诺创建];
C1 --> G[自定义对象迭代];
C2 --> H[生成序列数据];
D --> I[结束];
E --> I;
F --> I;
G --> I;
H --> I;
6. 总结与展望
通过本文的介绍,我们深入了解了JavaScript中的异步编程、迭代器和生成器技术。异步编程让我们能够更好地处理异步操作,避免阻塞主线程;迭代器和生成器为处理序列数据提供了强大的工具,使代码更加灵活和高效。
在未来的JavaScript开发中,这些技术将继续发挥重要作用。随着Web应用的不断发展,异步操作会越来越常见,正确处理异步错误和优化异步代码将成为开发者的重要技能。同时,迭代器和生成器也将在数据处理、算法实现等领域得到更广泛的应用。开发者可以进一步探索这些技术的高级应用,如使用
async
/
await
结合生成器实现更复杂的异步迭代等。
希望本文能帮助读者更好地掌握这些技术,在实际开发中编写出更优秀的JavaScript代码。
超级会员免费看
308

被折叠的 条评论
为什么被折叠?



