看一下回调

24 阅读5分钟

回调函数的本质是:把一个函数作为参数传递给另一个函数,在合适的时机(比如异步操作完成、某个事件触发)由后者调用这个函数。前端中回调的实现方式主要分为基础回调、高阶用法和现代语法糖(Promise/async-await本质也是基于回调的封装)。


一、回调函数的核心本质(先理解基础)

回调函数的核心逻辑是「委托执行」:

// 核心结构:函数A接收函数B作为参数,在A内部调用B 
function 主函数(回调函数) { 
    // 执行主逻辑(同步/异步) 
    回调函数(/* 传递参数 */); // 在合适时机调用回调 
} 
// 使用:传入回调函数 
主函数(function 回调函数(参数) { 
    // 回调逻辑 
}); 

二、回调的具体实现方式(按场景分类)

1. 同步回调(立即执行)

场景:数组方法(forEach/map/filter)、简单的逻辑委托(如数据处理完成后回调)。 特点:回调函数在主函数执行过程中立即同步执行,无延迟。

示例1:自定义同步回调函数

// 主函数:处理数据,完成后调用回调返回结果 
function processData(data, callback) { 
    // 同步处理数据(比如过滤、转换) 
    const processed = data.map(item => item * 2); 
    // 调用回调,传递处理后的结果 
    callback(processed); 
} 
// 使用:传入回调函数 
const rawData = [1, 2, 3]; 
processData(rawData, function(result) { 
    console.log('处理后的数据:', result); // 输出:[2,4,6] 
}); 
// 箭头函数简化回调(更常用) 
processData(rawData, (result) => { 
    console.log('箭头函数回调:', result); // 输出:[2,4,6] 
}); 

示例2:数组内置同步回调(高频使用)

const arr = [1, 2, 3, 4]; 

// forEach 回调:遍历每个元素 
arr.forEach((item, index) => { 
    console.log(`索引${index}的值:${item}`); 
}); 

// filter 回调:过滤符合条件的元素 
const evenArr = arr.filter((item) => item % 2 === 0);
console.log(evenArr); // [2,4] 

// map 回调:转换元素 
const doubleArr = arr.map((item) => item * 2); 
console.log(doubleArr); // [2,4,6,8] 

2. 异步回调(延迟执行)

场景:定时器、事件监听、AJAX请求、文件读写(Node.js)等异步操作。 特点:回调函数不会立即执行,而是等待异步操作完成后触发,这是回调最核心的使用场景。

示例1:定时器回调(setTimeout/setInterval)

// setTimeout:延迟1秒执行回调 
setTimeout(() => { 
    console.log('1秒后执行的回调'); 
}, 1000); 

// 带参数的异步回调 
function delayCallback(time, callback) { 
    setTimeout(() => {
        callback(`延迟${time}ms后执行`); 
    }, time); 
} 

// 使用 
delayCallback(1500, (msg) => { 
    console.log(msg); // 1500ms后输出:延迟1500ms后执行 
}); 

示例2:DOM事件监听回调(前端最常用)

<button id="btn">
    点击我
</button> 
<script> 
    const btn = document.getElementById('btn'); 
    
    // 方式1:匿名函数回调 
    btn.addEventListener('click', () => { 
        console.log('按钮被点击(匿名回调)'); 
    }); 
    
    // 方式2:命名函数回调(便于移除监听)
    function handleClick() { 
        console.log('按钮被点击(命名回调)'); 
    }
    
    btn.addEventListener('click', handleClick); 
    
    // 移除回调(必须用命名函数) 
    // btn.removeEventListener('click', handleClick); 
</script> 

示例3:AJAX请求回调(经典异步场景)

// 原生XMLHttpRequest回调 
function requestData(url, successCallback, errorCallback) { 
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onload = function() {
        if (xhr.status >= 200 && xhr.status < 300) { 
            // 成功回调:传递响应数据
            successCallback(JSON.parse(xhr.responseText));
        } else { 
            // 失败回调:传递错误信息 
            errorCallback(new Error(`请求失败:${xhr.status}`)); 
        } 
    }; 
    xhr.onerror = function() { 
        errorCallback(new Error('网络错误')); 
    }; 
    xhr.send(); 
} 
// 使用:传入成功/失败回调 
requestData( 
    'https://jsonplaceholder.typicode.com/todos/1', 
    (data) => { 
        console.log('请求成功:', data); // 输出返回的todo数据 
    },
    (error) => { 
        console.error('请求失败:', error); 
    } 
); 

3. 回调的进阶用法

(1)错误优先回调(Node.js规范,前端也常用)

核心规则:回调函数的第一个参数固定为错误对象(无错误则为null),第二个及以后参数为成功数据。 优势:统一错误处理逻辑,可读性更高。

// 模拟读取文件(Node.js风格) 
function readFile(filename, callback) { 
    // 模拟异步读取 
    setTimeout(() => { 
        if (filename === 'test.txt') { 
            // 无错误:第一个参数为null,第二个为数据 
            callback(null, '文件内容:Hello World'); 
        } else { 
            // 有错误:第一个参数为错误对象 
            callback(new Error('文件不存在'), null); 
        } 
    }, 1000); 
} 
// 使用:错误优先回调 
readFile('test.txt', (err, data) => { 
    if (err) { 
        // 优先处理错误 
        console.error('读取失败:', err); 
        return; 
    } 
    // 处理成功数据 
    console.log('读取成功:', data);
});
readFile('none.txt', (err, data) => { 
    if (err) { 
        console.error('读取失败:', err); // 输出:读取失败:Error: 文件不存在 
        return; 
    } 
    console.log(data); 
}); 

(2)回调地狱(问题场景)与解决

回调地狱:多层异步回调嵌套,导致代码可读性差、维护困难。

// 回调地狱示例:多层嵌套 
setTimeout(() => { 
    console.log('第一步:获取用户ID'); 
    const userId = 1; 
    setTimeout(() => { 
        console.log(`第二步:根据ID${userId}获取用户信息`); 
        const user = { id: 1, name: '张三' }; 
        setTimeout(() => { 
            console.log(`第三步:获取${user.name}的订单`); 
            const orders = [{ id: 101, goods: '手机' }];
            console.log('最终结果:', orders); 
        }, 1000); 
    }, 1000); 
}, 1000);

解决方式1:拆分命名函数

// 拆分回调为命名函数,减少嵌套 
function getUserId(callback) { 
    setTimeout(() => { 
        console.log('第一步:获取用户ID'); 
        callback(1); 
    }, 1000); 
} 
function getUserInfo(userId, callback) { 
    setTimeout(() => { 
        console.log(`第二步:根据ID${userId}获取用户信息`); 
        callback({ id: userId, name: '张三' }); 
    }, 1000); 
}
function getOrders(user, callback) { 
    setTimeout(() => { 
        console.log(`第三步:获取${user.name}的订单`); 
        callback([{ id: 101, goods: '手机' }]); 
    }, 1000); 
} 
// 链式调用(无嵌套) 
getUserId((userId) => { 
    getUserInfo(userId, (user) => { 
        getOrders(user, (orders) => { 
            console.log('最终结果:', orders); 
        }); 
    });
});

解决方式2:Promise封装(现代主流) Promise本质是回调的「优雅封装」,将嵌套回调转为链式调用:

// 用Promise封装异步操作 
function getUserId() { 
    return new Promise((resolve) => { 
        setTimeout(() => { 
            console.log('第一步:获取用户ID'); 
            resolve(1);
        }, 1000); 
    }); 
} 
function getUserInfo(userId) {
    return new Promise((resolve) => {
        setTimeout(() => { 
            console.log(`第二步:根据ID${userId}获取用户信息`);
            resolve({ id: userId, name: '张三' }); 
        }, 1000); 
    }); 
}
function getOrders(user) { 
    return new Promise((resolve) => { 
        setTimeout(() => {
            console.log(`第三步:获取${user.name}的订单`);
            resolve([{ id: 101, goods: '手机' }]); 
        }, 1000); 
    });
} 
// 链式调用(无嵌套) 
getUserId()
    .then((userId) => getUserInfo(userId))
    .then((user) => getOrders(user))
    .then((orders) => {
        console.log('最终结果:', orders);
    });

解决方式3:async/await(语法糖,最简洁) async-await是Promise的语法糖,彻底消除回调写法,转为同步风格:

// 基于上面的Promise函数,使用
async-await async function fetchData() { 
    const userId = await getUserId(); 
    const user = await getUserInfo(userId); 
    const orders = await getOrders(user); 
    console.log('最终结果:', orders); 
} 
fetchData();

4. 自定义可控回调(带条件/上下文)

示例1:带执行条件的回调

// 只有满足条件才执行回调 
function checkPermission(role, callback) { 
    const adminRoles = ['admin', 'super_admin'];
    if (adminRoles.includes(role)) { 
        callback(null, '权限通过'); 
    } else { 
        callback(new Error('无权限'), null); 
    } 
}
// 使用 
checkPermission('admin', (err, msg) => { 
    if (err) { 
        console.error(err); return; 
    } 
    console.log(msg); // 输出:权限通过 
});
checkPermission('user', (err, msg) => { 
    console.error(err); // 输出:Error: 无权限 
});

示例2:绑定this上下文的回调

const obj = { 
    name: '测试对象', 
    execute(callback) { 
        // 方式1:用call/apply绑定this 
        callback.call(this);

        // 方式2:用bind绑定(返回新函数) 
        // const boundCallback = callback.bind(this); 
        // boundCallback(); 
    } 
}; 
// 回调函数需要访问obj的this 
obj.execute(function() { 
    console.log(this.name); // 输出:测试对象(若不绑定则为undefined) 
}); 

总结

前端回调函数的核心知识点和实现方式:

  1. 核心本质:函数作为参数传递,在主函数的指定时机执行,分为同步/异步两类。
  2. 基础实现
  • 同步回调:数组方法(forEach/map)、自定义同步逻辑委托;
  • 异步回调:定时器、事件监听、AJAX请求(前端核心场景)。
  1. 进阶用法
  • 错误优先回调:第一个参数为错误对象(Node.js规范,前端也常用);
  • 回调地狱:多层嵌套导致可读性差,可通过拆分命名函数、Promise、async-await解决。
  1. 关键细节:回调中this上下文需要手动绑定(call/apply/bind),否则会丢失。 回调是前端异步编程的基础,Promise和async-await都是对回调的封装和优化,理解回调的本质能帮你更好地掌握现代异步语法。