对象混合和克隆
对象混合实际就是两个对象,将他们的属性混合后产生一个。我们可以这么说,如果说把对象 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 非常好用,完全可以用来当做一个全局环境来使用,并且这个环境并不污染其他空间,并且这个环境只有自己能访问到,也非常私密和安全。