深浅拷贝
直接复制对象存在的问题:
简单数据类型会将数值直接存储在栈中,但是!引用类型不会直接将值存储在栈中,而是以地址的形式存储,然后具体的数值存放在堆中。当我把obj赋值给o,实际上,赋值的是(栈)地址。因此我无论修改obj还是o,他们的地址永远不会发生改变,永远指向同一个数值。
浅拷贝
浅拷贝和深拷贝:都只针对于--【引用类型】
浅拷贝:拷贝的是【地址】
常见方法:
1.拷贝对象:Object.assign() / 展开运算符{...obj}拷贝对象
2.拷贝数组:Array.prototype.concat() / [...arr]
const pink = {
name:'pink老师',
age:18
}
const red = {...pink}
console.log(red)//{name:'pink老师',age:18}
o.age = 20
console.log(o)//{name:'pink老师',age:20}
console.log(obj)//{name:'pink老师',age:18}
-----------------------------------------------------------
Object.assign(red,pink)//将pink对象拷贝给red对象
console.log(red) //{name:'pink老师',age:18}
red.name = 'red老师'
console.log(red) //{name:'red老师',age:18}
//不会影响pink对象
console.log(pink) //{name:'pink老师',age:18}
虽然上方,修改数据时,看不出来问题,但是真正的问题的下方:
const obj = {
name:'pink老师',
age:18,
//对象的里面,再嵌套一个对象
family:{
baby:'小pink'
}
}
const o = {}
Object.assign(o,obj)
o.family.baby = '老pink'
console.log(o)
console.log(obj)
按道理,我只修改了o里面的【baby对象】,只有o里面的值会改变,而obj里面的值不会改变。但是,实际上,obj和o里面的值都发生了改变!!!
因为在对象里面再创建一个对象,此时又双叒叕创建了一个地址
浅拷贝:只适合于拷贝单层的,深层在创建对象,拷贝的时候就会存在问题。
如果是简单数据类型,那就拷贝值,引用数据类型,拷贝的就是地址(简单理解:如果是单层对象,没问题,如果是多层、就有问题)
直接赋值和浅拷贝的区别 直接赋值:只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址。
浅拷贝:如果是一层对象,不相互影响,如果出现多层对象拷贝,还是会相互影响的。
浅拷贝的理解 拷贝对象之后,里面的属性值是【简单数据类型】直接拷贝值
如果属性值是【引用数据类型】则直接拷贝地址。
深拷贝
浅拷贝和深拷贝都是只针对引用类型
深拷贝:拷贝的是对象,而不是地址。
常见方法:
1.通过递归实现深拷贝
2.lodash/cloneDeep
3.通过JSON.stringify()实现
递归函数
如果一个函数在内部,可以调用本身,那么这个函数就是递归函数。
let i = 1
function fn(){
if(i>=6){
return
}
i++
//在自己的函数里面,再调用自己;一定要写退出条件!!!
fn()
}
//fn()
由于递归很容易发生:“栈溢出”错误,所以必须要加退出条件return
递归函数常用场景
利用递归函数实现:setTimeout模拟setInterval的效果。
function getTime(){
//拿到当前时间
new Date().toLocaleString()
//每隔一秒钟调用这个函数
setTimeout(getTime,1000)
}
getTime()
递归函数实现深拷贝
浅拷贝的写法实现:
const obj = {
uname:'pink',
age:18
}
const o = {}
// 拷贝函数
function deepCopy(newObj,oldObj){
//遍历对象
for(let k in oldObj){
//k是oldObj中的key
//值:oldObj[k]
newObj[k] = oldObj[k] //给新对象添加属性
}
}
deepCopy(o,obj)//o是新对象,obj是旧对象
console.log(o)//{uname:'pink',age:18}
o.age = 20 //{uname:'pink',age:20}
console.log(obj){uname:'pink',age:18}
一开始我的newObj里是空,newObj[k]实现【创建key】但是value是空(uname: __),而newObj[k] = oldObj[k]实现了赋值,所以直接实现了创建+赋值的操作。
但是上方写的代码是一个浅拷贝,也就是说如果书写了复杂数据类型,仍然赋值的是地址。
深拷贝:
const obj = {
uname:'pink',
age:18,
hobby:['乒乓球','足球']
}
const o = {}
//拷贝函数
function deepCopy(newObj,oldObj){
for(let k in oldObj){
//判断 value 是简单数据类型还是复杂数据类型
if(oldObj[k] instanceof Array){
//新让新数组为空,然后再将遍历出来的数据存进去
//hobby: __
newObj[k] = []
//oldObj[k]本质就是['乒乓球','篮球']
//再次遍历 递归
deepCopy(newObj[k],oldObj[k])
}else if(oldObj[k] instanceof Object){
newObj[k] = {}
deepCopy(newObj[k],oldObj[k])
}
else{
newObj[k] = oldObj[k]
}
}
}
deepCopy(o,obj)
o.age = 20
o.hobby[0] = '篮球'
console.log(obj)
注意:一定要先写Array的判断,再写Object的判断,因为Object中包含Array。
lodash实现深拷贝
js库中lodash里面的cloneDeep内部实现了深拷贝。
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。
格式:_.cloneDeep(value)
<body>
//先引入
<script src = './lodash.min.js'></script>
<script>
const obj = {
uname:'pink',
age:18,
hobby:['乒乓球','足球'],
family:{
baby:'小pink'
}
}
//深拷贝
const o = _.cloneDeep(obj)
o.family.baby = '老pink'
console.log(o) //值变成老pink
console.log(obj) //值没变
</script>
</body>
JSON实现深拷贝
const obj = {
uname:'pink',
age:18,
hobby:['乒乓球','足球'],
family:{
baby:'小pink'
}
}
//把对象转换成字符串
JSON.stringify(obj)
//字符串转换为对象
const o = JSON.parse(JSON.stringify(obj))
o.family.baby = '123'
console.log(obj)//obj里面的值没有被改
先把他转换为字符串,就是一个全新的数据了,在转换为对象,此时这个对象跟obj已经一点关系都没有了,是一个属于我o全新的数据。所以我随便修改o,跟obj一点关系都没有。
异常处理
异常处理是:预估代码在执行过程中,可能会发生的错误、然后最大程度的避免【错误的发生、导致整个程序无法继续运行】。
throw抛出异常
一个函数需要接收参数,但是我就是不传参数,那么空参就是undefined,undefined+undefined = NaN
function fn(x,y){
//如果没写参数,就是undefined,undefined的取反,就是真
//这里是为了判断,我是不是传了一个空参
if(!x || !y){
throw new Error('没有参数传递进来')
}
return x + y
}
fn()
throw一般和new Error('')搭配使用,因为new Error会告诉程序员,具体哪一行出现了问题。
一旦我throw抛出异常,那么throw后面的所有代码都不会执行,会中断执行!!!
try/catch捕获异常
1.我们可以通过try/catch捕获错误信息(浏览器提供的错误信息)
2.将预估可能发生错误的代码写在try代码段中
3.如果try代码中出现了错误,那么就会执行catch代码段,并截获到错误信息
4.finally是不管程序是否有没有错误,都会执行的代码段。
function fn(){
try{
//可能有问题的代码,写在try中
const p = document.querySelector('p')
p.style.color = 'red'
}catch(err){
console.log(err.message)
return //中断程序
}finally{
//不管程序有没有错误,不管有没有中断函数,我都一定会执行的代码段
alert('执行')
}
//有错的话,我函数中的代码就不会被执行到
console.log(111)
}
fn()
catch后面要写一个括号:catch(),里面书写一个自定义名字的参数,他是浏览器反馈给我们的错误信息。
catch不会中断程序,需要手写一个return;而throw会直接中断程序。
debugger
在程序中,可能存在报错的位置写一个debugger,和在控制台中打断点的效果是一样的。
普通函数this
普通函数中的this值:谁调用this的值,就指向谁
//普通函数
function sayHi(){
console.log('Hi')
}
sayHi() //this执行window
const obj = {
sayHi:function(){
}
}
obj.sayHi() //是obj调用,所以this是obj
补充:use strict开启严格模式,普通函数中、没有明确调用者的时候,this指向是window,而严格模式下的this值,在没有人调用的时候,是undefined。
箭头函数this
箭头函数中的this与普通函数完全不一样,也不受到调用方式的影响,事实上,箭头函数中并不存在this!!!
1.箭头函数会默认帮我们绑定上外层的this的值,所以在箭头函数中,this的值和外层的this是一样的。
2.箭头函数中的this引用的,就是最近作用域中的this。
3.向外层作用域中,一层一层的查找this,直到有this的定义。
注意: 在开发中【使用箭头函数前,要考虑函数中this的值】,事件回调函数中,使用箭头函数时,this的指向是全局的window;因此DOM事件回调函数、【如果里面需要DOM对象的this,则不推荐使用箭头函数】。
function Person(){}
//原型对象上添加箭头函数
Person.prototype.walk = ()=>{
console.log(this)//window
}
const p1 = new Person()
p1.walk()
walk里面的this指向的是原型对象的this指向,就不会再是实例对象p1了。
构造函数、原型对象、DOM事件函数,这些都不推荐使用箭头函数
改变this
JavaScript中,允许去指定 函数中的this指向,有3个方法可以动态的去指定普通函数中的this指向:
1.call()
2.apply()
3.bind()
call()了解
call()使用的比较少,作为了解即可。
语法:fun.call(thisArg,arg1,arg2)
thisArg:在fun函数运行时,指定this的值
arg1、arg2:传递的其他参数
返回值就是函数的返回值,因为他就是调用函数。
const obj = {
uname:'pink'
}
function fn(x,y){
console.log(this)//window
console.log(x+y)//3
}
call()的两个作用:
//1.调用函数
fn.call(1,2)//默认就是调用函数 等价于:fn(1,2)
//2.改变this指向
fn.call(obj,1,2)//现在指向的是obj,1和2是参数
apply()重要
apply比较重要,但是bind()是最重要的
使用apply()来调用函数,同时指定被调用函数中的this的值。
语法:fun.apply(thisArg,[argsArray])
thisArg:在fun函数运行时,指定的this值
argsArray:传递的值,必须包含在数组里面
返回值就是函数的返回值,因为它就是调用函数
因此apply主要跟数组有关系,必须使用Math.max()求数组的最大值。
const obj = {
age:18
}
function fn(){
console.log(this)//window
}
//让fn指向obj
fn.apply(obj)//this=obj
-----------------------------------------
function fn(x,y){
console.log(this)
console.log(x+y)//3
}
fn.apply(obj,[1,2])//可以接一个数组
call()和apply()的区别就是:一个传的普通参数,一个传递的是数组。
相同点:都可以用来调用函数(书写后,立马调用),都可以改变this的指向。
apply()可以来求数组的最大值:
const arr = [3,5,2,9]
console.log(Math.max(...arr))
//apply(this指向谁,数组参数)
const max = Math.max.apply(Math,arr)
const min = Math.min.apply(Math,arr)
apply(null):this指向可以为空,但是不可以省略!
bind()很重要的
bing()方法不会调用函数,但是可以改变函数内部的this指向。
语法:fun.bind(thisArg,arg1,arg2, ...)
thisArg:在fun函数运行时,指定this的值
arg1,arg2:传递的其他参数
返回是由指定的this值和初始化参数改造的【原函数拷贝】(新函数)
因此当我们只是想改变this指向,并不想调用这个函数的时候,可以使用bind,比如改变定时器内部的this指向。
const obj = {
age:18
}
function fn(){
console.log(this)
}
//1.bind不会调用函数
fn.bind()//没有输出,因为他不会调用函数!
//2.改变this指向
fn.bind(obj)
//3.bing返回值:返回一个新函数(拷贝之前的改过this的函数)
const fun = fn.bind(obj)
console.log(fun)//和fn()函数一模一样,但是this指向obj
fun()
bind()应用
我们只是想改变this指向,并不想调用这个函数的时候,可以使用bind()。
最典型的应用:定时器;因为定时器我们希望他过一会儿再调用。
需求:有一个按钮,点击-就禁用、2秒后在开启
<button></button>
const btn = document.querySelector('button')
btn.addEventListener('click',function(){
//禁用按钮--this =》btn
this.disabled = true
//2s后恢复状态
setTimeout(function(){
this.disabled = false
},2000)
})
但是实际上,他会一直禁用,没有执行到setTimeout中。因为setTimeout中的this指向的是调用者,也就是window.setTimeout.
要么改成://btn.disabled = false,要么改成箭头函数,要么就使用bind()
window.setTimeout(function(){
//this由原来的window改为btn
this.disabled = false
}.bind(btn),2000)
因为bind自身是不会主动调用的,所以可以通过定时器来延迟后,调用。
还可以这样修改:
btn.addEventListener('click',function()){
this.disabled = true
window.setTimeout(function(){
this.disabled = false
}.bind(this),2000)
}
bind(this),因为这个方法是写在函数外面的,所以他的this是和外面的this.disabled = true的this是同一个指向,也就是btn。
三者区别
相同点:
都可以用来改变函数内部的this指向
不同点:
call和apply会调用函数,并且改变函数内部的this指向
call和apply传递的参数不一样,call传递的是arg1,arg2...的形式,而apply必须传入[arg]数组的形式
bind不会调用函数,但是可以改变函数内部的this指向
主要应用场景:
call调用函数并且可以传递函数
apply经常跟数组有关系,比如借助数学对象实现数组的最大值和最小值
bind不调用函数,但是还想改变this的指向,比如改变定时器内部的this指向
节流(用的更多)
节流:连续触发事件,但是在n秒中,只能执行1次函数
比如说:轮播图,我快速点击切换,如果没有写节流的话,那么就会:点几下、我就切换几下,但是容易造成卡顿和切换失败的现象、并且严重消耗资源。
如果我写了节流:你在1s中点了无数下,我也规定在1s后才会切换到下一张,而不是你点击下,我就切换几张。
节流案例
const box = document.querySelector('.box')
let i = 1
// 鼠标移动函数
function mouseMove(){
box.innerHTML = ++i
}
// 事件监听
box.addEventListener('mousemove',mouseMove)
利用节流的方式:鼠标经过、500ms,数字才会显示。
核心思路:利用时间相减--:移动后的时间 - 刚开始移动的时间 > = 500ms 我才去执行mouseMove函数
1.写一个节流函数throttle,来控制这个函数(mouseMove),500ms之后才去执行这个函数。
2.节流函数传递2个参数,第一个参数是mouseMove函数,第二个参数是指定时间500ms。
3.鼠标移动事件,里面写的是节流函数
const box = document.querySelector('.box')
let i = 1
// 鼠标移动函数
function mouseMove(){
box.innerHTML = ++i
}
-------------------------------------
//节流函数throttle
function throttle(fn,t){
}
-------------------------------------
// 事件监听
box.addEventListener('mousemove',throttle(mouseMove,500))
我们说在事件监听里面,使用函数时,不能写小括号,因为等同于调用函数操作。就会丧失事件触发机制,无法多次调用执行。
//节流函数throttle
function throttle(fn,t){
return function(){
console.log(i)
}
}
// 事件监听
box.addEventListener('mousemove',throttle(mouseMove,500))
这样就可以避免丧失触发机制。原理:函数调用的时候,会接收到一个函数的返回值,这里的返回值是:function(){ console.log(i) }是一个函数,并不是一个调用。等价于:box.addEventListener('mousemove',function(){console.log(i)})
let i = 1
//鼠标移动函数
function mouseMove(){
box.innerHTML = ++i
//如果里面存在大量dom操作 的情况,可能会卡顿
}
//节流函数throttle
function throttle(fn,t){
//页面一打开就会存在的时间
let startTime = 0
return function(){
//鼠标移入时,得到当前时间戳
let now = Date.now
//判断如果大于等于500 才去调用函数
if(now - startTime >= t){
//只有过了500ms,我才会调用这个鼠标移动函数
fn()
//因为我的let startTime,调用函数只会执行一次。让起始时间等于上一次的触发时间
startTime = now
}
}
}
box.addEventListenter('mousemove',throttle(mouseMove,500))
防抖
防抖:在触发时间后的n秒内,函数只会执行一次,如果在n秒内又触发了事件,那么就会【重新计算】函数的执行时间。
防抖最典型的应用:搜索框,当我在搜索框中输入的时候,其实就已经在请求数据了,然后会有下拉提示。但是如果我输一个字,就请求一次,非常的消耗资源,因此我们做到:在输入后,然后间歇1s,才请求数据。这期间再写其他的我就会重新计时。
案例:鼠标在盒子上移动,鼠标停止后,500ms后里面的数字就会变化+1。(移动的时候不给你触发,必须你停止滑动500ms才给你触发)
核心思路:利用定时器实现,当鼠标划过,判断有没有定时器,还有就清除,以最后一次滑动为准,开启定时器。
const box = document.querySelector('.box')
let i = 1
//鼠标移动函数
function mouseMove(){
box.innerHTML = ++i
}
//防抖函数
function debounce(fn,t){
let timeId
return function(){
//滑动期间:如果有定时器就清除
if(timeId){
clearTimeout(timeId)
}
timeId = setTimeout(function(){
fn()
},t)
}
}
box.addEventListener('mousemove',debounce(mouseMove,200))
lodash节流和防抖
节流和防抖的区别:
节流:连续触发事件,但是在n秒钟只会执行一次的函数,比如可以利用节流实现:1s之内,只会触发一次鼠标移动事件。
防抖:如果在n秒内又触发了事件,则会重新计算函数执行的时间。
节流和防抖的使用场景:
节流:鼠标移动,页面尺寸发生变化,滚动条滚动等开销比较大的情况。
防抖:搜索框输入,设定每次输入完毕n秒后发送请求,如果期间还有输入,则从新计算时间。
节流:_.throttle(方法,时间)
function mouseMove(){
box.innerHTML = ++i
}
box.addEventListener('mousemove',_.throttle(mouseMove,300))
防抖:_.debounced(方法,时间)
function mouseMove(){
box.innerHTML = ++i
}
box.addEventListener('mousemove',_.debounced(mouseMove,300))
节流综合案例
页面一打开,可以记录上一次视频播放的位置:
两个事件:
ontimeupdate:事件在视频/音频 当前播放位置发生改变的时候,会触发(视频播放才触发)
onloadeddata:事件在当前帧的数据加载完毕且还没有足够的数据播放视频/音频的下一帧的时候会触发。(页面一打开视频的时候触发)
ontimeupdate的触发频次太高,我们可以对他设定节流,每1秒钟只触发一次。
在ontimeupdate事件触发的时候,每隔1s,就记录当前时间到本地储存。
下次打开页面,onloaddate事件触发,就可以从本地存储中取出时间,让视频从取出的时间播放,如果没有,默认为0s
获得当前时间:video.currentTime
video是一个标签;
//1获取元素 要对视频进行操作
const video = document.querySelector('video')
//视频音频比较特殊,只能这样触发事件
video.ontimeupdate = _.throttle(()=>{
//把当前时间存储到本地存储中
localStorage.setItem('currentTime',video.currentTime)
},1000)
//打开页面触发事件,就从本地存储里面取出记录的时间,赋值给video.currentTime
video.onloaddata = ()=>{
//如果没有,就默认从0s开始
localStorage.getItem('currentTime') || 0
}