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 - 1) in 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 === false) break;
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 === null) return null;
if (typeof obj !== "object") return obj;
// 过滤掉特殊的对象(正则对象或者日期对象):直接使用原始值创建当前类的一个新的实例即可,这样克隆后的是新的实例,但是值和之前一样
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return 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 排版