JS回调函数(利用回调函数封装_each方法,内含深比较、浅比较)

2,217 阅读6分钟

JS回调函数

简单理解为:把一个函数当做值传递给另外一个函数,在另外一个函数中把这个函数执行,执行的这个函数就是“回调函数”;在JS中我们约定俗称的把回调函数形参写为callback

  • 我们常用的回调函数
    • 数组中的迭代方法(forEach、replace... )
    • 定时器(setTimeout、setInterval)
    • JQ中ajax请求中success函数
    • DOM2事件(addEventListenter)
function func(callback{
 // callback => anonymous
 for (let i = 0; i < 5; i++) {
  // callback(i); //=>分别把每一次循环的I的值当做实参传递给anonymous,所以anonymous总计被执行了5次,每一次执行都可以基于形参index获取到传递的i的值
  let res = callback.call(document, i);
  // res是每一次anonymous执行返回的结果
  if (res === false) {
   // 接受回调函数返回的结果,控制循环结束
   break;
  };
 };
};

func(function anonymous(index{
 if (index >= 3) {
  return false;
 }
 return '@' + index;
});

在func函数执行的过程中,我们可以“尽情”的操作这个回调函数

  • 可以把它执行零到多次
  • 还可以给回调函数传递实参
  • 还可以改变里面的THIS
  • 还可以接受函数执行的返回结果

为了更好的应用回调函数,我们用回调函数封装一个强大的_each方法

  • 思路:
    • _EACH([VALUE],[CALLBACK],[CONTEXT]),我们可以传三个参数
    • 可以遍历数组、类数组、对象,每一次遍历都可以把[CALLBACK]执行
    • 每一次执行回调函数,都会把当前遍历的结果(当前项\索引)传递给回调函数
    • 支持第三个参数,用来改变回调函数中的THIS指向(不传递,默认是WINDOW)
    • 支持回调函数返回值,每一次返回的值会把当前集合中的这一项的值替换掉;如果回调函数返回的是FALSE(一定是FALSE),则结束遍历
// 检测是否为数组或者类数组
function isArrayLike(obj{
 let length = !!obj && ("length" in obj) && obj.length;
 return Array.isArray(obj) || length === 0 || (typeof length === "number" && length > 0 && (length - 1in obj);
}

function _each(obj, callback, context = window{
 obj = _cloneDeep(obj); //=>把原始传递的进来的数据深度克隆一份,后期操作的都是克隆后的结果,对原始的数据不会产生改变

 // 参数合法性校验
 if (obj == null) {
  //=>null undefined  
  // 手动抛出异常信息,一但抛出,控制台会报错,下面代码不在执行 Error/TypeError/ReferenceError/SyntaxError...
  throw new TypeError('OBJ必须是一个对象/数组/类数组!');
 }
 if (typeof obj !== "object") {
  throw new TypeError('OBJ必须是一个对象/数组/类数组!');
 }
 if (typeof callback !== "function") {
  throw new TypeError('CALLBACK必须是一个函数!');
 }

 // 开始循环(数组和类数组基于FOR循环,对象循环是基于FOR IN)
 if (isArrayLike(obj)) {
  // 数组或者类数组
  for (let i = 0; i < obj.length; i++) {
   // 每一次遍历都执行回调函数,传递实参:当前遍历这一项和对应索引
   // 而且改变其THIS
   // RES就是回调函数的返回值
   let res = callback.call(context, obj[i], i);
   if (res === false) {
    // 返回FALSE结束循环
    break;
   }
   if (res !== undefined) {
    // 有返回值,则把当前数组中的这一项替换掉
    obj[i] = res;
   }
  }
 } else {
  // 对象
  for (let key in obj) {
   if (!obj.hasOwnProperty(key)) break;
   let res = callback.call(context, obj[key], key);
   if (res === falsebreak;
   if (res !== undefined) obj[key] = res;
  }
 }
 return obj;
}

封装_each方法中还用到了_cloneDeep函数,此函数也是我们自己封装的方法

深克隆、浅克隆也是我们面试中经常问到的题目,在这里我们也进一步了解一下

  • 浅克隆:只把第一级的拷贝一份赋值给新数组,一般我们实现数组克隆的办法都是浅克隆
  • 深克隆:不仅把第一级克隆一份给新数组,如果原始数组中存在多级,那么是把没一级都克隆一份赋值给新数组的每一级别
  • 实现浅克隆的方法
let arr = [10,20,[30,40]];
let arr1 = arr.slice(0);//用数组中的slice方法赋值一份赋值给新数组arr1
let arr2 = arr.concat();//用数组中的concat方法拼接一个数组,也相当于复制一份
let arr3 = [...arr];//用剩余运算符将arr赋值一份到新数组
  • 实现深克隆
//JSON.stringify(arr) 把原始对象变为一个字符串(去除堆和堆嵌套的关系)
//JSON.parse(...) 在把字符串转换为新的对象,这样浏览器会重新开辟内存来存储信息
let arr4 = JSON.parse(JSON.stringify(arr));

但是JSON.stringify并不是对所有的值都能有效处理:

  • 正则变为空对象
  • 函数/undefined/Symbol都会变为null
  • 日期格式数据变为字符串后,基于PARSE也会不到日期对象格式了
  • 但是对于数字/字符串/布尔/null/普通对象/数组对象等都没有影响
  • 这样克隆后的信息和原始数据产生差异化
  • 所以我们封装一个可以进行深克隆的方法
function _cloneDeep(obj{
 // 传递进来的如果不是对象,则无需处理,直接返回原始的值即可(一般Symbol和Function也不会进行处理的)
 if (obj === nullreturn null;
 if (typeof obj !== "object"return obj;

 // 过滤掉特殊的对象(正则对象或者日期对象):直接使用原始值创建当前类的一个新的实例即可,这样克隆后的是新的实例,但是值和之前一样
 if (obj instanceof RegExpreturn new RegExp(obj);
 if (obj instanceof Datereturn new Date(obj);

 // 如果传递的是数组或者对象,我们需要创建一个新的数组或者对象,用来存储原始的数据
 // obj.constructor 获取当前值的构造器(Array/Object)
 let cloneObj = new obj.constructor;
 for (let key in obj) {
  // 循环原始数据中的每一项,把每一项赋值给新的对象
  if (!obj.hasOwnProperty(key)) break;
  cloneObj[key] = _cloneDeep(obj[key]);
 }
 return cloneObj;
}

本文使用 mdnice 排版