Loading...
墨滴

超超不开心。

2021/04/27  阅读:104  主题:默认主题

一天一个小知识(前端篇):JS回调函数

一天一个小知识(前端篇):JS回调函数

作为学习前端不是很久的小白(我本人)来说,读研期间一直有听女朋友(前端大佬)说Promise是为了避免回调地狱应运而生的。当时的我一直不明白什么叫回调函数,更不懂回调地狱这类概念(读研我本人做NLP的,本科期间写的前端js代码简直是💩嘛。)

今天,自己就系统的学习并总结一下,js的回调函数究竟是什么?

1 函数也是对象

首先,我们需要明确一个概念:

函数实际上也是对象,它们能被“存储”在变量中,能作为函数的参数被传递,能在函数中被创建,能从函数中返回。

2 什么是回调?

简单的来讲,回调是一个在另一个函数完成执行后所执行的函数——故此得名“回调”。

复杂的来说,在JS中,函数是对象。因此,一个函数(主体函数)可以将另一个函数作为参数,并且可以返回另一个函数。执行此操作的函数(主体函数)称为高阶函数。而任何作为参数传递的函数都称为回调函数。

3 回调函数的作用

(一)同步操作callback

在这个同步操作中,我们感受一下回调函数“里应外合”的作用。

// 定义myFilter方法
Array.prototype.myFilter = function (callback) {
  // 此方法返回一个新数组,先定义好
  let res = [];
  for (let i = 0; i < this.length; i++) {
    item = this[i];
    if (callback(item, i)) { 
     // 这里的封装是在调用回调函数(这里就是“里应”),里面的参数item,i都是实参
    // 又因为这里调用时约定好了传参顺序,也就决定了定义时的形参顺序。
      res.push(item);
    }
  }
  return res;
}

// test
let arr = [1, 2, 3, 4, 0];

// 这里是在定义回调函数,是按照封装时的约定来传参的,这里就是“外合”
let res = arr.myFilter((item, index) => {
  return (item % 2 === 0);
})

console.log(res);

由此可见,回调函数不同于一般函数,它一般是先约定好如何调用,再在使用时定义的。在自己封装的时候,可以看看如何约定更方便。

在这个例子中,callback的执行条件是调用myFilter方法时,执行目的是返回一个Boolean值,用于myFilter方法内的if判断。

(二)异步操作callback

在异步操作中,无法将结果return出来(因为你后续的代码是同步的,会直接先执行。如果你后续的代码需要异步操作的结果,那无法等到异步操作的结果return完,后续的操作就结束了)。因此,我们可以想到用回调函数来解决这一问题。

下面举了一个详细的例子来说明。

举例

  • 未使用回调函数的异步操作
function myAjax(method, url, parameter) {
  let xhr = new XMLHttpRequest();
  if(method.toUpperCase() == 'GET' && parameter != undefined) {   // 含GET请求
    url += parameter; 
  }

  xhr.open(method, url, true);  // 第三个参数为true,表示JavaScript异步执行,不等待后台返回
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      let obj = JSON.parse(this.responseText);
      return obj;
    }
  }

  if(method.toUpperCase() == 'POST' && parameter != undefined) {   // 含GET请求
    xhr.send(parameter);
  } else {
    xhr.send();
  }
}

let obj = myAjax('GET',url,parameter);
console.log(obj);   // undefined

解析:

此时,我们在外部调用myAjax方法,用的是ajax异步请求服务器。除xhr.onreadystatechange函数外,其它都是同步执行的。 当js引擎在遇见onreadystatechange触发事件时,会先将该事件放到异步任务队列里面,然后继续向下执行,而到了**console.log(obj)**这个同步操作的时候,由于异步执行的原因,此时的问题就出现了,即当前onreadystatechange的还没有执行。所以console.log(obj)根本无法获取到异步操作return的obj对象,会输出undefined。

我们接着再继续尝试使用回调函数来完成异步操作。

  • 使用回调函数的异步操作
function myAjax(method, url, callback, parameter) {
  let xhr = new XMLHttpRequest();
  if(method.toUpperCase() == 'GET' && parameter != undefined) {   // 含GET请求
    url += parameter; 
  }

  xhr.open(method, url, true);  // 第三个参数为true,表示JavaScript异步执行,不等待后台返回
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      let obj = JSON.parse(this.responseText);
      callback(obj);
    }
  }

  if(method.toUpperCase() == 'POST' && parameter != undefined) {   // 含GET请求
    xhr.send(parameter);
  } else {
    xhr.send();
  }
}

myAjax("GET", url, function(data) {
  let h = document.getElementsByTagName("h1")[0];
  if (data.code == 404) {
    h.innerHTML = "";
    h.innerHTML = data.msg;
  } else {
    console.log("success");
  }
}, parameter);   // success

解析: 此时在调用myAjax的时候,callback回调函数是与其他代码同步执行的(异步任务队列中的宏任务),即使myAjax方法里面的onreadystatechange被放到了异步任务队列中,但此时的myAjax方法也必须等onreadystatechange事件执行完成后将值反馈给回调函数callback才会结束myAjax方法里面的程序。 因此使用回调函数在这个异步操作中我们就可以取到obj的值了。

4 回调地狱

在使用JavaScript时,为了实现某些逻辑经常会写出层层嵌套的回调函数,如果嵌套过多,会极大影响代码可读性和逻辑,这种情况也被成为回调地狱。比如说你要把一个函数A作为回调函数,但是该函数又接受一个函数B作为参数,甚至B还接受C作为参数使用,就这样层层嵌套,人称之为回调地狱,代码阅读性非常差。

let sayhello = function (name, callback) {
  setTimeout(function () {
    console.log(name);
    callback();
  }, 1000);
}
sayhello("first"function () {
  sayhello("second"function () {
    sayhello("third"function () {
      console.log("end");
    });
  });
});
//输出: first second third end

解决回调地狱的方法

解决回调地狱有很多方法,比如:Promise对象、Generator函数、async函数 举一个Promise解决的例子

  • Promise解决回调地狱
let sayhello = function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(name);
      resolve();  //在异步操作执行完后执行 resolve() 函数
    }, 1000);
  });
}
sayhello("first").then(function () {
  return sayhello("second");  //仍然返回一个 Promise 对象
}).then(function () {
  return sayhello("third");
}).then(function () {
  console.log('end');
}).catch(function (err) {
  console.log(err);
})
//输出:first second third end

后续也会有更详细介绍Promise对象的文章。

超超不开心。

2021/04/27  阅读:104  主题:默认主题

作者介绍

超超不开心。