JavaScript 中的上下文问题解决与异步数据获取
1. 解决上下文问题
在 JavaScript 中,函数创建新上下文时可能会导致意外结果,特别是在回调或数组方法中使用
this
关键字时,上下文的改变容易造成混淆。这种问题在类中同样存在。
1.1 上下文问题示例
以一个验证器为例,最初将其作为对象字面量创建,现在可以将其转换为类。以下是验证器类的代码:
class Validator {
constructor() {
this.message = 'is invalid.';
}
setInvalidMessage(field) {
return `${field} ${this.message}`;
}
setInvalidMessages(...fields) {
return fields.map(this.setInvalidMessage);
}
}
当调用
setInvalidMessages()
方法时,会出现上下文问题。
map()
方法调用
setInvalidMessage()
时,会在数组方法的上下文中创建一个新的
this
绑定,而不是类的上下文,从而导致错误。示例代码如下:
const validator = new Validator();
validator.setInvalidMessages('city');
// TypeError: Cannot read property 'message' of undefined
1.2 解决方法
1.2.1 使用箭头函数
可以将方法转换为箭头函数,箭头函数不会创建新的
this
绑定,从而避免错误。示例代码如下:
class Validator {
constructor() {
this.message = 'is invalid.';
this.setInvalidMessage = field => `${field} ${this.message}`;
}
setInvalidMessages(...fields) {
return fields.map(this.setInvalidMessage);
}
}
这种方法的缺点是,在类语法中,需要将函数移到属性而不是方法中,可能会导致方法定义分散,构造函数变得庞大。
1.2.2 使用
bind()
方法
bind()
方法存在于所有函数中,可以显式指定上下文。示例代码如下:
function sayMessage() {
return this.message;
}
const alert = {
message: 'Danger!',
};
const sayAlert = sayMessage.bind(alert);
sayAlert();
// Danger!
在验证器类中,可以在将函数传递给
map()
方法之前,将其绑定到当前上下文:
class Validator {
constructor() {
this.message = 'is invalid.';
}
setInvalidMessage(field) {
return `${field} ${this.message}`;
}
setInvalidMessages(...fields) {
return fields.map(this.setInvalidMessage.bind(this));
}
}
为了避免多次绑定,可以在构造函数中将绑定的方法设置为同名属性:
class Validator {
constructor() {
this.message = 'is invalid.';
this.setInvalidMessage = this.setInvalidMessage.bind(this);
}
setInvalidMessage(field) {
return `${field} ${this.message}`;
}
setInvalidMessages(...fields) {
return fields.map(this.setInvalidMessage);
}
}
1.3 未来规范
在未来的规范中,可以在构造函数外部设置类属性,将箭头函数分配给属性,与其他方法定义一起使用,这是一种更好的解决方案。示例代码如下:
class Validator {
message = 'is invalid.';
setMessage = field => `${field} ${this.message}`;
setInvalidMessages(...fields) {
return fields.map(this.setMessage);
}
}
目前可以使用适当的 Babel 插件来使用此功能,但该功能在任何版本的 Node.js 中都不受支持。
2. 异步数据获取
2.1 异步编程基础
JavaScript 是一种异步语言,意味着它可以在等待请求数据时继续执行后续代码。异步语言的价值在于,如果代码的某些部分不需要延迟的信息,可以在其他代码等待时运行这些部分。
2.2 回调函数处理异步数据
在引入 Promise 之前,开发者使用回调函数来处理异步操作。例如,使用
setTimeout()
函数模拟异步操作:
function getUserPreferences(cb) {
setTimeout(() => {
cb({
theme: 'dusk',
});
}, 1000);
}
function log(value) {
return console.log(value);
}
log('starting');
// starting
getUserPreferences(preferences => {
return log(preferences.theme.toUpperCase());
});
log('ending?');
// ending
// DUSK
回调函数是处理异步数据的一种方式,但当存在多个嵌套的异步函数时,会出现“回调地狱”问题,代码变得难以阅读和维护。
2.3 Promise 解决回调问题
Promise 是一种处理异步请求的 JavaScript 方法,它可以解决回调问题。Promise 有成功和失败的方法,并且可以链式调用异步 Promise,使代码更加清晰。
2.3.1 Promise 基本用法
以下是使用 Promise 的示例代码:
function getUserPreferences() {
const preferences = new Promise((resolve, reject) => {
resolve({
theme: 'dusk',
});
});
return preferences;
}
getUserPreferences()
.then(preferences => {
console.log(preferences.theme);
});
// 'dusk'
在设置 Promise 时,应该同时设置
then()
和
catch()
方法,分别处理成功和失败的情况。示例代码如下:
function failUserPreference() {
const finder = new Promise((resolve, reject) => {
reject({
type: 'Access Denied',
});
});
return finder;
}
failUserPreference()
.then(preferences => {
// This won't execute
console.log(preferences.theme);
})
.catch(error => {
console.error(`Fail: ${error.type}`);
});
// Fail: Access Denied
2.3.2 链式调用 Promise
可以将多个 Promise 链式调用,将数据通过一系列的
then()
方法传递。示例代码如下:
function getMusic(theme) {
if (theme === 'dusk') {
return Promise.resolve({
album: 'music for airports',
});
}
return Promise.resolve({
album: 'kind of blue',
});
}
getUserPreferences()
.then(preference => {
return getMusic(preference.theme);
})
.then(music => {
console.log(music.album);
});
// music for airports
还可以使用箭头函数简化代码:
getUserPreferences()
.then(preference => getMusic(preference.theme))
.then(music => { console.log(music.album); });
并且,在链式调用 Promise 时,可以只定义一个
catch()
方法来处理任何 Promise 被拒绝的情况。示例代码如下:
function getArtist(album) {
return Promise.resolve({
artist: 'Brian Eno',
});
}
function failMusic(theme) {
return Promise.reject({
type: 'Network error',
});
}
getUserPreferences()
.then(preference => failMusic(preference.theme))
.then(music => getArtist(music.album))
.catch(e => {
console.log(e);
});
2.4 Promise 总结
Promise 是一种强大的工具,可以处理多种情况,并且具有简单的接口。还有一个
Promise.all
方法,可以接受一个 Promise 数组,并在所有 Promise 完成时返回一个解决或拒绝的结果。
2.5 流程图
graph LR
A[开始] --> B[调用 getUserPreferences()]
B --> C{Promise 是否成功}
C -- 成功 --> D[执行 then() 方法]
D --> E[调用 getMusic()]
E --> F{Promise 是否成功}
F -- 成功 --> G[执行 then() 方法]
F -- 失败 --> H[执行 catch() 方法]
C -- 失败 --> H[执行 catch() 方法]
2.6 表格总结
| 处理方式 | 优点 | 缺点 |
|---|---|---|
| 回调函数 | 简单直接 | 容易出现“回调地狱”,代码难以维护 |
| Promise | 解决回调地狱问题,代码清晰,可链式调用 | 语法相对复杂 |
3.
async/await
语法简化异步操作
虽然 Promise 已经极大地改善了异步代码的可读性,但 ES2017 引入的
async/await
语法让异步代码更加直观,就像编写同步代码一样。
3.1
async/await
基本概念
async
函数总是返回一个 Promise。在
async
函数内部,可以使用
await
关键字来暂停函数的执行,直到 Promise 被解决或拒绝。
3.2 使用
async/await
重写示例
以下是使用
async/await
重写前面的
getUserPreferences
和
getMusic
示例:
function getUserPreferences() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
theme: 'dusk',
});
}, 1000);
});
}
function getMusic(theme) {
return new Promise((resolve) => {
if (theme === 'dusk') {
resolve({
album: 'music for airports',
});
} else {
resolve({
album: 'kind of blue',
});
}
});
}
async function main() {
try {
const preferences = await getUserPreferences();
const music = await getMusic(preferences.theme);
console.log(music.album);
} catch (error) {
console.error(error);
}
}
main();
在这个示例中,
main
函数被标记为
async
,在函数内部使用
await
关键字等待
getUserPreferences
和
getMusic
的 Promise 解决。如果任何一个 Promise 被拒绝,
try...catch
块会捕获错误。
3.3
async/await
与 Promise 链式调用对比
- Promise 链式调用 :
getUserPreferences()
.then(preference => getMusic(preference.theme))
.then(music => console.log(music.album))
.catch(error => console.error(error));
-
async/await:
async function main() {
try {
const preference = await getUserPreferences();
const music = await getMusic(preference.theme);
console.log(music.album);
} catch (error) {
console.error(error);
}
}
main();
可以看到,
async/await
语法让代码更像是同步代码,减少了嵌套,提高了可读性。
3.4 流程图
graph LR
A[开始] --> B[调用 main() 函数]
B --> C[进入 async 函数]
C --> D[await getUserPreferences()]
D --> E{Promise 是否成功}
E -- 成功 --> F[await getMusic()]
F --> G{Promise 是否成功}
G -- 成功 --> H[输出音乐专辑信息]
G -- 失败 --> I[捕获错误并输出]
E -- 失败 --> I[捕获错误并输出]
3.5 表格对比
| 处理方式 | 代码可读性 | 错误处理 | 适用场景 |
|---|---|---|---|
| Promise 链式调用 | 相对复杂,嵌套较多时不易阅读 | 需要在每个链式调用中处理错误或在末尾统一处理 | 多个异步操作按顺序执行,逻辑较简单 |
async/await
| 代码更像同步代码,可读性高 |
使用
try...catch
统一处理错误,更直观
| 异步操作较多,逻辑复杂,需要清晰的错误处理 |
4. 浏览器数据存储
在处理外部数据时,有时需要在浏览器中存储数据,以便在不访问服务器的情况下保持用户状态。以下介绍几种常见的浏览器数据存储方式。
4.1
localStorage
localStorage
是一种持久化的存储方式,数据会一直保留在浏览器中,直到手动清除。
4.1.1 存储数据
localStorage.setItem('theme', 'dusk');
4.1.2 获取数据
const theme = localStorage.getItem('theme');
console.log(theme);
4.1.3 删除数据
localStorage.removeItem('theme');
4.2
sessionStorage
sessionStorage
与
localStorage
类似,但数据仅在当前会话期间有效,关闭浏览器窗口或标签页后数据会被清除。
4.2.1 存储数据
sessionStorage.setItem('username', 'john_doe');
4.2.2 获取数据
const username = sessionStorage.getItem('username');
console.log(username);
4.2.3 删除数据
sessionStorage.removeItem('username');
4.3 表格对比
| 存储方式 | 数据有效期 | 存储容量 | 数据共享范围 |
|---|---|---|---|
localStorage
| 持久化,直到手动清除 | 约 5MB | 同一域名下的所有页面共享 |
sessionStorage
| 当前会话期间,关闭窗口或标签页清除 | 约 5MB | 同一窗口或标签页内共享 |
4.4 流程图
graph LR
A[开始] --> B{选择存储方式}
B -- localStorage --> C[存储数据]
B -- sessionStorage --> D[存储数据]
C --> E[获取数据]
D --> E[获取数据]
E --> F{是否需要删除数据}
F -- 是 --> G[删除数据]
F -- 否 --> H[结束]
G --> H[结束]
5. 总结
在处理 JavaScript 中的上下文问题时,可以使用箭头函数或
bind()
方法来明确
this
的指向。对于异步数据的获取,从早期的回调函数到 Promise,再到
async/await
语法,不断地在提高代码的可读性和可维护性。同时,利用浏览器的
localStorage
和
sessionStorage
可以方便地存储用户状态数据。在实际开发中,应根据具体需求选择合适的方法和技术,以提高开发效率和用户体验。
通过本文的介绍,相信你对 JavaScript 中的上下文处理、异步数据获取以及浏览器数据存储有了更深入的理解。希望这些知识能帮助你在开发中写出更高效、更健壮的代码。
超级会员免费看

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



