Skip to content

对象混合和克隆 ​

对象混合实际就是两个对象,将他们的属性混合后产生一个。我们可以这么说,如果说把对象 A 混合到对象 B 中,就是把 A 中的所有属性拿出来放到 B 中去,如果有重名属性会进行覆盖,对象混合其实非常简单。

js
        /**
         * 将obj2混合到obj1,返回一个新的对象。这个函数不会修改原有的obj1和obj2对象。
         * @param {Object} obj1 - 第一个对象,混合的源对象。
         * @param {Object} obj2 - 第二个对象,将被混合到obj1中的对象。
         * @returns {Object} - 返回一个新的对象,包含obj1和obj2的所有属性。
         */
        function mixmin(obj1, obj2) {
            var newObj = {}; // 创建一个新对象来存储混合后的属性。
            // 遍历obj1的所有可枚举属性,复制到新对象中。
            for (var key in obj1) {
                if (obj1.hasOwnProperty(key)) {
                    newObj[key] = obj1[key];
                }
            }
            // 遍历obj2的所有可枚举属性,复制到新对象中。
            for (var key in obj2) {
                if (obj2.hasOwnProperty(key)) {
                    newObj[key] = obj2[key];
                }
            }
            return newObj; // 返回混合后的新对象。
        }

实际上,JS 中给我们提供了一个 Object.assign() 方法,这个方法可以用来混合两个对象,即 Object.assign(obj1,obj2),也可以把 obj2 混合到 obj1 中去。这个函数存在一个小缺陷,即混合之后 obj1 本身也发生了变化。这个函数可以有多个参数,Object.assign(obj1,obj2,obj3) 实际上,这就是先让 obj 3 混合到 obj 2 中,再让 obj 2 混合到 obj 1 中,并且自始至终都只有 obj 1 发生了变化,所以我们可以使用 Object.assign({},obj1,obj2) 来获得一个混合后的新对象,并且 obj 1 和 obj 2 都不会发生变化。

混合在一些时候是很有用的,比如如果我们参数里有一个配置对象 config,这个对象可能有非常多的属性,并且这些属性都有各种各样的默认值,我就可以把 config 对象的所有属性的默认值写成一个对象,然后再用用户传入的参数来混合这个配置对象,就可以很轻松地完成给配置对象赋默认值的操作。

而对象克隆,就是产生一个一模一样的对象在上面的 mixmin 函数中,就是用 for...in 循环实现了克隆:

js
        /**
         * 克隆对象或数组。
         * @param {Object|Array} obj 要克隆的对象或数组。
         * @param {boolean} deep 是否进行深度克隆。当为true时,会递归克隆对象中的对象或数组;否则,仅克隆引用。
         * @returns {Object|Array} 返回obj的克隆副本。如果obj是数组,则返回一个新的数组;如果obj是对象,则返回一个新对象;其他类型直接返回本身。
         */
        function clone(obj, deep) {
            // 如果obj是数组,则返回其浅拷贝
            if (Array.isArray(obj)) {
            	if(deep){
            		var newArr = [];
            		for(var i = 0;i<obj.length;i++){
            			newArr.push(clone(obj[i],deep));
            		}
            		return newArr;
            	}else{
            		return obj.slice();
            	}
            } else if (typeof obj === "object") {
                // 如果obj是对象,创建一个新对象并递归复制属性
                var newObj = {};
                for (var key in obj) {
                    // 仅复制obj自身的属性,排除原型链上的属性
                    if (obj.hasOwnProperty(key)) {
                        newObj[key] = deep ? clone(obj[key],true) : obj[key]; // 如果deep为true,则对属性值进行深度克隆
                    }
                }
                return newObj;
            } else {
                // 对于非对象非数组类型,直接返回本身
                return obj;
            }
        }

这里实际上只是加上了一些判断,以增强程序的健壮性。

函数防抖 ​

如果我要做一个电梯程序,有人进入三秒后关门,但是现在有一个问题,如果这三秒中之内还有人进入,我是不是应该重新计时三秒钟,这样才比较合理。同样,在前端中,我希望用户输入结束之后做一些事,但是用户输入这个事件其实很频繁,我不确定他什么时候结束,比如用户输入一个 hello world,这里其实有很多个字符,我希望是他输入结束之后做一些事,而不是每一次输入字符都做一些事。这其实和电梯是一个道理,我可以假设用户三秒没有输入就是输入完毕,用户输入 h 的时候,开始计时三秒,然后输入了 e,此时应该重新开始计时三秒,后面同理,直到用户输入完毕之后三秒钟内没有再输入,我才会执行,要实现这个效果,就必须要用到防抖函数。

又比如我希望用户鼠标移动的时候做一些事,但是 mousemove 事件运行的非常频繁,如果每一次 mousemove 都去运行一次,不管是观感还是性能都不太好,所以很多时候希望用户鼠标移动之后等个半秒钟我再执行这个事件,这其实就是防抖函数要干的事。

所以,防抖函数要干的事就是在触发事件后,让函数等待一会儿再运行,如果等待过程中又触发了这个事件,则重置等待。在实际应用中,我们会遇到很多触发非常频繁的事件,比如鼠标移动,窗口尺寸改变,滚动条,如果我不希望它被这么频繁的触发就需要用到函数防抖。

js
/**
 * 函数防抖动封装。
 * 函数防抖动技术是指当一个函数连续被触发时,只有当触发间隔超过指定时间后,该函数才会被执行。
 * 这在处理高频触发事件(如窗口大小改变、滚动等)时非常有用,可以避免因频繁执行操作(如DOM操作)而导致的性能问题。
 * @param {Function} fn 需要被延迟执行的函数。
 * @param {number} duration 指定的延迟时间,单位为毫秒。
 * @returns {Function} 返回一个包装后的函数,该函数具有防抖功能。
 */
function debounce(func, delay) {
	let timer = null;
	return function () {
		clearTimeout(timer);
		timer = setTimeout(() => {
			func.apply(this, arguments);
		}, delay);
	};
}

这个代码其实很好理解,无非就是返回一个进过防抖处理的函数,当频繁调用这个函数的时候,这个函数不会立刻被执行,而是会在停止调用后的一断时间内执行。

这种在函数内部返回的函数,我们叫做高阶函数

this 可以在使用函数的时候使用 bind 进行绑定,实际上,在使用函数的时候作为参数传递 this 是更好的一种办法。防抖函数是必须要掌握的内容,其中参数传递很容易遗漏,需要着重关注。

函数节流 ​

函数节流是保证一个时间段内只执行一次。

这里就有两种思路,第一种是函数运行之后立即执行,并且在指定时间内阻止改函数被重复调用。第二种思路是函数运行后等待一段时间再执行,并且无视期间产生的其他调用。

js
/**
 * 函数节流器 - 控制函数调用频率
 * @param {Function} fn 要节流的函数
 * @param {number} duration 函数调用最小间隔时间(毫秒)
 * @returns {Function} 返回一个新函数,新函数会控制原函数的调用频率
 */
function throttle1(fn, duration) {
	var flag = false;
	return function () {
		// 当flag为false时,允许调用原函数
		if (!flag) {
			flag = true; // 标记为true,防止其他调用
			var args = arguments; // 存储当前调用的参数
			fn.apply(null, args); // 使用apply调用原函数
			setTimeout(function () {
				flag = false; // 函数调用完成后,重置flag为false
			}, duration);
		}

	}
}
/**
 * 函数节流器 - 控制函数调用频率(第二种实现方式)
 * @param {Function} fn 要节流的函数
 * @param {number} duration 函数调用最小间隔时间(毫秒)
 * @returns {Function} 返回一个新函数,新函数会控制原函数的调用频率
 */
function trrottle2(fn, duration) {
	var flag = false;
	return function () {
		// 当flag为false时,允许调用原函数
		if (!flag) {
			flag = true; // 标记为true,防止其他调用
			var args = arguments; // 存储当前调用的参数
			setTimeout(function () {
				fn.apply(null, args); // 使用apply调用原函数
				flag = false; // 函数调用完成后,重置flag为false
			}, duration)
		}
	}
}
  • throttle1 函数在调用时立即执行原函数,然后设置一个延时,延时结束后重置标志位,允许下一次调用。
  • throttle2函数在调用时先设置标志位,然后延时执行原函数,延时结束后重置标志位,允许下一次调用。

函数节流的思想在于上锁,通过函数防抖和函数节流希望读者能对闭包有个新的认识,从本质上来说,由于函数运行完之后其上下文就被销毁掉了,但是如果一个函数在内部又定义了一个子函数,此时子函数的作用域是可以拿到自己的 VO 的,如果正常执行结束,该函数其实也会被销毁,但是如果这个子函数被保存在了其他地方,或者被返回了,这就产生了闭包,此时自己的上下文虽然已经没了,但是 VO 却被原来的函数拿到了,里面的属性也成为了类似静态属性的东西。

所以闭包这种现象本质上是函数或者对象被保存到了其他地方导致 VO 无法释放的一种现象。此时这个 VO 非常好用,完全可以用来当做一个全局环境来使用,并且这个环境并不污染其他空间,并且这个环境只有自己能访问到,也非常私密和安全。

以笔记为镜,可以知得失