16、JavaScript 中的上下文问题解决与异步数据获取

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 中的上下文处理、异步数据获取以及浏览器数据存储有了更深入的理解。希望这些知识能帮助你在开发中写出更高效、更健壮的代码。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值