深克隆 VS 浅克隆|深比较 VS 浅比较|回调函数

2,504 阅读10分钟

上一篇文章 案例|原生手写一个轮播图——渐隐渐显版 还有很多不足,这里要非常感谢大佬 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;
}

好了,基础知识部分我们先铺垫这些;

下一篇主要做:左右轮播图的插件封装(只是简单的思考与模仿)

笔者深知未来的路还很长,插件封装是一项重要课题,虽然能力有限,但要敢于尝试,希望能在各位大佬的监督下与大家一起成长😄