JS高级-4

123 阅读15分钟

深浅拷贝

直接复制对象存在的问题:

image.png

简单数据类型会将数值直接存储在栈中,但是!引用类型不会直接将值存储在栈中,而是以地址的形式存储,然后具体的数值存放在堆中。当我把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里面的值都发生了改变!!!

因为在对象里面再创建一个对象,此时又双叒叕创建了一个地址

image.png

浅拷贝:只适合于拷贝单层的,深层在创建对象,拷贝的时候就会存在问题。

如果是简单数据类型,那就拷贝值,引用数据类型,拷贝的就是地址(简单理解:如果是单层对象,没问题,如果是多层、就有问题)

直接赋值和浅拷贝的区别 直接赋值:只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址。
浅拷贝:如果是一层对象,不相互影响,如果出现多层对象拷贝,还是会相互影响的。

浅拷贝的理解 拷贝对象之后,里面的属性值是【简单数据类型】直接拷贝值
如果属性值是【引用数据类型】则直接拷贝地址。

深拷贝

浅拷贝和深拷贝都是只针对引用类型
深拷贝:拷贝的是对象,而不是地址。

常见方法:
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一点关系都没有。

异常处理

异常处理是:预估代码在执行过程中,可能会发生的错误、然后最大程度的避免【错误的发生、导致整个程序无法继续运行】。

image.png

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后面的所有代码都不会执行,会中断执行!!!

image.png

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,和在控制台中打断点的效果是一样的。

image.png

普通函数this

普通函数中的this值:谁调用this的值,就指向谁

//普通函数
function sayHi(){
    console.log('Hi')
}

sayHi() //this执行window

const obj = {
    sayHi:function(){
        
    }
}
obj.sayHi() //是obj调用,所以this是obj

image.png

补充:use strict开启严格模式,普通函数中、没有明确调用者的时候,this指向是window,而严格模式下的this值,在没有人调用的时候,是undefined。

image.png

箭头函数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事件函数,这些都不推荐使用箭头函数

image.png

改变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。

三者区别

image.png

相同点:
都可以用来改变函数内部的this指向

不同点:
call和apply会调用函数,并且改变函数内部的this指向
call和apply传递的参数不一样,call传递的是arg1,arg2...的形式,而apply必须传入[arg]数组的形式
bind不会调用函数,但是可以改变函数内部的this指向

主要应用场景:
call调用函数并且可以传递函数
apply经常跟数组有关系,比如借助数学对象实现数组的最大值和最小值
bind不调用函数,但是还想改变this的指向,比如改变定时器内部的this指向

节流(用的更多)

节流:连续触发事件,但是在n秒中,只能执行1次函数

比如说:轮播图,我快速点击切换,如果没有写节流的话,那么就会:点几下、我就切换几下,但是容易造成卡顿和切换失败的现象、并且严重消耗资源。

如果我写了节流:你在1s中点了无数下,我也规定在1s后才会切换到下一张,而不是你点击下,我就切换几张。

image.png

节流案例

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
}