5、深浅拷贝
5.1、数据类型
数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。
基本数据类型的特点:直接存储在栈(stack)中的数据
引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
5.2、深浅拷贝区别
深浅拷贝主要区别在复杂数据类型,对于基本数据类型,没有区别,改变拷贝的数据,都不会改变原数据
浅拷贝(shallow copy):
浅拷贝只拷贝引用(地址值),当拷贝的新对象发生改变时,原对象也会发生相同的改变,也就是说,浅拷贝会影响原来的元素
深拷贝(deep copy):
每一级的数据都会拷贝 ,拷贝后,两个对象拥有不同的地址,当拷贝出来的对象发生改变时,原对象内容不会改变,两者互不影响
5.3、实现浅拷贝
5.3.1、直接赋值法
var arr = [1,2,3]
var newarr = arr;
newarr[1] = 5;
console.log(arr,newarr);//[1, 5, 3],[1, 5, 3]
5.3.2、Object.assign()
<script>
// 1.Object.assign()
var obj = {
name: "jack",
age: 18,
person: {
name: "tim",
age: 28,
},
};
//将{}和obj合并,返回一个新的obj对象
var objNew = Object.assign({}, obj);
objNew.name = "tom";//当object只有一层的时候,是深拷贝
objNew.person.name = "Diana";//对object多层的数据,是浅拷贝
console.log(obj, "obj");
console.log(objNew, "objNew");
</script>
5.3.3、Array.prototype.concat()
//3、Array.prototype.concat()
var arr2 = [1, "hello", { usename: "tom" }];
var arr3 = arr2.concat();
arr2[2].usename = "tim";
console.log(arr2[2].usename, "arr2"); //tim
console.log(arr3[2].usename, "arr3"); //tim
5.3.4、Array.prototype.slice()
var arr4 = [1, "hello", { usename: "tom" }];
var arr5 = arr4.slice();
arr4[2].usename = "diana";
//console.log(arr4[2].usename, "arr4"); //diana
//console.log(arr5[2].usename, "arr5"); //diana
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
5.4、实现深拷贝
5.4.1、Object.assign()
当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
<script>
//1、Object.assign();
var obj = {
//当object只有一层的时候,是深拷贝
name: "jack",
age: 18,
};
var objNew = Object.assign({}, obj);
objNew.name = "tom";
console.log(obj.name, "obj"); //jack
console.log(objNew.name, "objNew"); //tom
</script>
5.4.2、JSON.parse(JSON.stringify())
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝
注意:这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
<script>
var arr1 = [
1,
"hello",
{ usename: "tom" },
function () {
console.log(111);
},
];
var arr2 = JSON.parse(JSON.stringify(arr1));
arr1[2].usename = "diana";
console.log(arr1[2].usename); //diana
console.log(arr2[2].usename); //tom
console.log(arr2[3]);//null
</script>
5.4.3、手写递归方法
原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
<script>
function deepClone(oldData) {
// 1、判断oldData的数据类型
if (typeof oldData == "object" && oldData !== null) {
// 3、再判断类型是否是数组,根据类型返回[],{},开辟新的空间,放深拷贝数据
var res = Array.isArray(oldData) ? [] : {};
// 4、遍历数据,拿到属性,将属性值赋值给拷贝的数据
for (var k in oldData) {
// 5、判断这个k属性是否是oldData上自有的属性,原型链上的就不算了
if (oldData.hasOwnProperty(k)) {
// 6、对象有可能是嵌套对象,所以,需要递归再进行判断,一层层,直到拷贝了所有的数据
res[k] = deepClone(oldData[k]);
}
}
return res;
} else {
// 2、如果不是对象或数组,则返回原数据
return oldData;
}
}
// 测试:
let obj = {
name: "jack",
age: 18,
hobby: ["song", "run"],
sayHi(msg) {
console.log(msg);
},
};
let newObj = deepClone(obj);
// obj.sayHi("hi");
// newObj.sayHi("hello");
newObj.name = "tom";
newObj.hobby = ["唱歌", "跑步"];
console.log(obj, "obj");
console.log(newObj, "newObj");
</script>
5.4.4、通过jQuery的extend方法实现深拷贝
var array = [1,2,3,4];
var newArray = $.extend(true,[],array);
5.4.5、lodash函数库实现深拷贝
lodash很热门的函数库,提供了 lodash.cloneDeep()实现深拷贝
6、防抖节流
6.1、节流和防抖的目的
都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,防止在短时间内频繁触发同一事件而出现延迟,假死或卡顿的现象
6.2、节流和防抖的区别
防抖:如果不断在delay之前重新触发,那么定时器会不断重新计时,最终会在最后一次完后才执行
节流:目前有一事件A设置了定时器,那么在delay之前触发,都只会触发一次
6.3、节流和防抖的详解
(1)、防抖 debounce(设置1分钟只会执行一次,如果1分钟内又多次触发,会从再次触发开始重新计算1分钟时间,然后再执行)
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
本质:将多次执行变为最后一次执行(重新执行)
举例:比如我们平时在使用搜索框时,我们一输入内容就会发送对应的网络请求,如果我们后面一直有在输入内容,那么就会一直发送网络请求。正确的做法应该是在我们输入期间不发送网络请求,当我们输入完成后在发送网络请求。
(2)、节流 throttle(设置1分钟只会执行一次,一分钟内,多次触发无效,必须等1分钟后才能触发函数)
高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率
本质:将多次执行变成每隔一段时间执行(不能打断我)
举例:比如我们在玩LOL游戏时,当需要回城时,我们触发回城按钮,进入到回城状态进会有一个等待的时间,如果在这个等待时间内有重复去执行回城按钮,这时回城状态并不会做出其他响应,而是等时间到了后才会完成回城。这也说明了,在我们回城的过程中,一直触发回城按钮都是不会响应的,他会按照自己的一个回城时间才做出响应。
6.4、代码实现简单防抖与节流
6.4.1、防抖
6.4.1.1、简单版(掌握)
<body>
<!-- 防抖函数 debounce -->
<input type="text" name="" id="" />
<script>
var int = document.querySelector("input");
function inputChange() {
console.log(this.value);
}
// 不加防抖函数,不断的输出
//int.addEventListener("keyup", inputChange);
//加入防抖函数,1秒钟输出一次
var intVal = debounce(inputChange, 1000);
int.addEventListener("keyup", intVal);
//防抖函数
function debounce(fn, delay) {
// 1、定义一个定时器,保存上一次的定时器
var timer = null;
// 2、真正执行的函数
var _debounce = function () {
//3、 取消上一次的定时器
if (timer) clearTimeout(timer);
//4、 保存this
var _this = this;
//5、 延迟执行
timer = setTimeout(function () {
//6、 让fn执行时,指向input
fn.call(_this);
}, delay);
};
return _debounce;
}
</script>
</body>
6.4.1.2、this和参数实现(了解)
<input type="text" name="" id="" />
<script>
var int = document.querySelector("input");
// 需求:输出输入的内容
function inputChange() {
console.log(this.value);
}
// 不加防抖函数,不停的搜索
// int.addEventListener("keyup", inputChange);
// 加入防抖函数
int.addEventListener("keyup", debounce(inputChange, 1000));
// 防抖函数二:this和参数实现
function debounce(fn, delay) {
//console.log(this, "debounce"); //这里的this指向的是window
// 1、定义一个定时器,保存上一次的定时器
var timer = null;
// 2、真正执行的函数,传入参数
return function (...ages) {
//3、保存this,此时的this指向的是input
var _this = this;
// 4、判断定时器是否存在,清楚定时器
if (timer) clearTimeout(timer);
// 重新调用setTimerout
timer = setTimeout(function () {
//console.log(this);//定时器里的this指向widow
// fn()直接执行,this指向window
fn.apply(_this, ages);
timer = null;
}, delay);
};
}
</script>
6.4.1.3、立即执行(了解)
<input type="text" name="" id="" value="你好" />
<script>
var int = document.querySelector("input");
// 需求:输出输入的内容
function inputChange() {
console.log(this.value);
}
// 不加防抖函数,不停的搜索
// int.addEventListener("keyup", inputChange);
// 加入防抖函数
int.addEventListener("keyup", debounce(inputChange, 1000, true));
// int.addEventListener("keyup", debounce(inputChange, 1000, false));
// 防抖函数三:立即执行
//创建一个防抖函数debounce
function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null;
let isInvoke = false;
// 2.真正执行的函数
const _debounce = function (...ages) {
// 取消上一次的定时器
if (timer) clearTimeout(timer);
// 判断是否需要立即执行
if (immediate && !isInvoke) {
fn.apply(this, ages);
isInvoke = true;
} else {
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数
fn.apply(this, ages);
isInvoke = false;
}, delay);
}
};
return _debounce;
}
</script>
6.4.2、节流
6.4.2.1、时间戳版(了解)
<body>
<!-- 需求:在快速点击的过程中,降低日志打印的频率,1s中执行一次 -->
<button>点我试试</button>
<script>
var btn = document.querySelector("button");
var fn = function () {
console.log("发送请求");
};
// 问题:快速点击按钮,只要点了一次,日志就打印一次
// btn.addEventListener("click", fn);
// 添加节流函数,在2s内,多次点击,只执行一次
btn.addEventListener("click", throttle(fn, 2000));
//参数: fn 真正执行的函数,interval 多久执行一次
function throttle(fn, interval) {
// 1、记录上一次的开始时间
var lastTime = 0;
// 2、事件触发时,真正执行的函数
var _throttle = function () {
// 2.1获取当前事件触发时的时间
var nowTime = new Date().getTime();
// 2.2使用规定好的时间间隔减去当前时间和上一次触发时间的时间间隔,得到多少时间再次触发
var remainTime = interval - (nowTime - lastTime);
if (remainTime <= 0) {
// 2.3触发真正函数
fn();
// 2.4 保留上次触发的时间
lastTime = nowTime;
}
};
return _throttle;
}
</script>
</body>
6.4.2.2、定时器版(掌握)
<body>
<!-- 需求:在快速点击的过程中,降低日志打印的频率,1s中执行一次 -->
<button>点我试试</button>
<script>
var btn = document.querySelector("button");
var fn = function () {
console.log("发送请求");
};
// 问题:快速点击按钮,只要点了一次,日志就打印一次
// btn.addEventListener("click", fn);
// 添加节流函数,在2s内,多次点击,只执行一次
btn.addEventListener("click", throttle(fn, 2000));
//参数: fn 真正执行的函数,interval 多久执行一次
// 定时器方式
function throttle(fn, delay) {
//1、设置标志,判断函数是否执行
var sign = true;
return function () {
//2、 在函数开头判断标志是否为 true,不为 true 则中断函数
if (!sign) return;
//3、 sign 设置为 false,防止执行之前再被执行
sign = false;
//4、 保存this
var _this = this;
setTimeout(function () {
//5、 this是当前被点击的dom元素,button
fn.apply(_this, arguments);
//6、 执行完事件之后,重新将这个标志设置为 true
sign = true;
}, delay);
};
}
</script>
</body>
6.5、应用场景
- 防抖
-
- 表单元素的校验,如手机号,邮箱,用户名等,部分搜索功能的模糊查询结果实现
- 搜索框搜素输入
- 文本编辑器实时保存
- 节流
-
- 高频事件,例如快速点击、鼠标滑动、resize事件、scroll事件
- 下拉加载
- 视频播放记录时间等