回调函数
上一篇文章 案例|原生手写一个轮播图——渐隐渐显版 还有很多不足,这里要非常感谢大佬 csdoker给出的宝贵意见和指导🙏,所以笔者决定重新完善一下轮播图的案例,打算做一个简易版的左右轮播图插件的封装;
想到笔者写文章的初衷是总结知识,争取做到通俗易懂,所以今天笔者打算,先铺垫几个需要用到的很重要的知识点:深克隆 VS 浅克隆
、深比较 VS 浅比较
、回调函数基础知识
;
一、深克隆 VS 浅克隆
思维导图
1、浅克隆
-1)定义:
只把第一级的拷贝一份,赋值给新的数组(一般我们实现数组克隆的办法都是浅克隆)
-2)方法:
slice:
- 实现克隆原理:创建一个新的数组,循环原始数组中的每一项,把每一项赋值给新数组
- let arr2 = arr1.slice(0);
concat:
- let arr2 = arr1.concat();
扩展运算符[…ary]:
- let arr2 = […arr1];
- ……等
2、深克隆
-1)定义:
不仅把第一级克隆一份给新的数组,如果原始数组中存在多级,那么是把每一级都克隆一份赋值给新数组的每一个级别
-2)方法一:利用 JSON 数据格式
语法:
let arr2 = JSON.parse(JSON.stringify(arr1))
;
实现原理:
- JSON.stringify(arr1):先把原始对象变为一个字符串(去除堆和堆嵌套的关系)
- JSON.parse(…):在把字符串转换为新的对象,这样浏览器会重新开辟内存来存储信息
应用:
- 对
数字
/字符串
/布尔
/null
/普通对象
/数组对象
等都没有影响,可以使用
- 对
缺点:
- JSON.stringify(arr1):并不是对所有的值都能有效处理
- 正则会变成空对象
- 函数/undefined/Symbol 都会变成null
- 这样克隆后的信息和原始数据产生差异化
- 日期格式数据变为字符串后,基于parse 也回不到对象格式了
- 举个🌰:一个变态的数组
let arr1 = [10, '20', [30, 40], /\d+/, function () {}, null, undefined, {
xxx: 'xxx'
}, Symbol('xxx'), new Date()];
复制代码
-3)方法二:自己封装
语法:
- let arr2 = cloneDeep(arr1);
思路:
1、传递进来的是函数时,不需要操作,直接返回即可
- 因为在一个执行环境栈中一个名字的函数只能又一个,如果我们自己又克隆了一个,会把原来的替换掉,这样做没有任何意义
2、传递进来的是基本数据类型时,不需要操作,直接返回即可
3、传递的是对象类型时
- (1). 正则对象:创建一个新的实例储存当前正则即可(因为我们的目的让空间地址不一样即可)
- (2). 日期对象:创建一个日期实例储存当前日期
- (3). 普通对象&&数组对象:创建一个新的实例,循环存储当前信息;
- 普通对象&&数组对象 中有可能还会存在多层嵌套的关系,所以这里我们可以用下递归
- 代码实现:
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;
}
复制代码
二、深比较 VS 浅比较
首先我们先来看下这是什么意思呢?
以题为例:
let obj1 = {
name: '小芝麻',
age: 10,
teacher: {
0: '张三',
1: '李四'
}
};
let obj2 = {
age: 20,
school: "北京",
teacher: {2: "王五"}
};
复制代码
当我们想要把上面两个对象合并的时候,就涉及到了“比较”的问题(笔者也不是很清楚为什么叫做“比较”);
- 两个对象中都有age、school、teacher属性;其中我们看见
teacher
的值是一个对象,而且内容还不一样,那当合并的时候,会是怎样的结果呢?
这就是我们接下来要说的深浅比较问题;
1、浅比较
-1)定义:
把两个对象合并为一个对象
-2)方法:Object.assign(obj1,obj2)
- 合并两个对象(用后一个替换前一个),返回合并后的新对象
- 这个方法中的合并就是浅比较:只比较第一级
还是这题
let obj1 = {
name: '小芝麻',
age: 10,
teacher: {
0: '张三',
1: '李四'
}
};
let obj2 = {
age: 20,
school: "北京",
teacher: {2: "王五"}
};
let obj = Object.assign(obj1,obj2);
console.log(obj);
复制代码
输出的结果如👇
可以看到合并两个对象(用后一个替换前一个)后,返回合并后的新对象;
其中同时共有的属性teacher
是一个对象数据类型,只比较了一级,就用后一项(obj2)对应的空间地址替换了前一项(obj1)的teacher
值的空间地址;
很多时候能们想要的效果并不是这样,我们想要的是把相同属性名对应的属性值也合并,就像上题中teacher
属性合并后应该是{0: '张三', 1: '李四', 2: "王五"}
,这个时候我们就需要进行深比较了
2、深比较
语法:
- let res = _assignDeep(obj1,obj2)
思路:
1、首先深克隆一份obj1
2、循环拿出obj2中的每一项与克隆的obj1比较,
- 如果当前拿出这一项是对象数据类型 并且 克隆的obj1 中相同属性名对应的也是对象数据类型的值,
- 再次进行深比较,用递归处理一下即可;
- 其余情况都直接用obj2的值替换obj1的值即可;
- 代码实现:
function _assignDeep(obj1, obj2) {
// 先把OBJ1中的每一项深度克隆一份赋值给新的对象
let obj = _cloneDeep(obj1);
// 再拿OBJ2替换OBJ中的每一项
for (let key in obj2) {
if (!obj2.hasOwnProperty(key)) break;
let v2 = obj2[key],
v1 = obj[key];
// 如果OBJ2遍历的当前项是个对象,并且对应的OBJ这项也是一个对象,此时不能直接替换,需要把两个对象重新合并一下,合并后的最新结果赋值给新对象中的这一项
if (typeof v1 === "object" && typeof v2 === "object") {
obj[key] = _assignDeep(v1, v2);
continue;
}
obj[key] = v2;
}
return obj;
}
复制代码
三、回调函数
约定俗成的回调函数形参名字:callback
思维导图
1、定义:
把一个函数当作值传递给另外开一个函数,在另外一个函数中把这个函数执行
2、特点
在大函数执行的过程中,我们可以“尽情”的操作传给他的回调函数
1、可以把它执行(执行零到多次)
2、还可以给回调函数传递实参
3、还可以改变里面的this
- 如果回调函数是一个箭头函数需要注意
- 箭头函数中没有THIS,用的THIS都是上下文中的
- 4、还可以接受函数执行的返回结果
function func(callback) {
// callback => anonymous
// 在FUNC函数执行的过程中,我们可以“尽情”的操作这个回调函数
// 1.可以把它执行(执行零到多次)
// 2.还可以给回调函数传递实参
// 3.还可以改变里面的THIS
// 4.还可以接受函数执行的返回结果
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) {
// console.log(index, this);
if (index >= 3) {
return false;
}
return '@' + index;
});
func((index) => {
// 箭头函数中没有THIS,用的THIS都是上下文中的
console.log(index, this);
});
复制代码
3、几个回调函数的经典用法
参数是回调函数的有很多
1、数组迭代的方法 forEach
- arr.forEach(item=>{})
- forEach在执行的时候,会遍历数组中的每一项,每遍历一项 会把我们传递进来的箭头函数执行一次
2、JQ中的ajax
- $.ajax({ url:’’, success:function(){ // 请求成功会把传递的函数执行 }});
3、事件绑定
- window.addEventListener(‘scroll’,function(){});
- ……等
4、封装一个迭代的方法(适用于:数组/类数组/对象)
定义:一个强大的迭代器
语法:_each([ARRAY/OBJECT/类数组],[CALLBACK])
@params:
- obj:要迭代的数组、类数组、对象
- callback:每一次迭代触发执行的回调函数
- 参数:item:当前项
- 参数:index:当前项索引
- context:要改变的回调函数的THIS
@return:返回处理后的新数组/对象
功能:
- 1、可以遍历数组、类数组、对象,每一次遍历都可以把【CALLBACK】执行
- 2、每一次执行回调函数,都会把当前遍历的结果(当前项/索引)传递给回调函数
- 3、支持第三个参数,用来改变回调函数中的THIS执行(不传递,默认是WINDOW)
- 4、支持回调函数返回值,每一次返回的值会把当前集合中的这一项的值替换掉;如果回调函数返回的是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;
}
复制代码
好了,基础知识部分我们先铺垫这些;