JS高阶技巧(深浅拷贝,节流,防抖)

JS高阶技巧

  1. 深入this学习,直到如何判断this指向和改变this指向
  2. 直到在JS中如何处理异常,学习深浅拷贝,理解递归

深浅拷贝

直接赋值,复制的地址,会影响原来的对象。所以有了深浅拷贝

首先浅拷贝和深拷贝只针对引用类型

浅拷贝

浅拷贝拷贝的是地址,浅浅地拷贝了一层

总结:

直接赋值和浅拷贝有什么区别?

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

浅拷贝怎么理解?

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

深拷贝

深拷贝拷贝的是对象,不是地址

常见方法(深拷贝三种实现方式):

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.stringfy()实现
通过递归实现深拷贝

在这里插入图片描述

利用递归函数实现setTimeout模拟setInterval效果

需求:

页面每隔1s输出当前时间

输出当前时间可以使用:new Date().toLocaleString()

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div></div>
    <script>
        function getTime(){
      
      
            document.querySelector('div').innerHTML=new Date().toLocaleString()
            setTimeout(getTime,1000)
        }
        getTime()
    </script>
</body>
</html>

简易深拷贝的函数 (对象,数组,二维数组,但如果对象数组之间相互引用,这个代码无法实现)

  //拷贝函数 简易的深拷贝(对象,数组,二维数组,但如果对象数组之间相互引用,这个代码无法实现)
        function deepCopy(newObj, oldObj) {
    
    
            for (let k in oldObj) {
    
    
                //处理数组的问题 把数组再次遍历 //递归思想
                //一定要把数组判断放在前面,不能颠倒。因为数组也属于对象,万物皆对象
                if (oldObj[k] instanceof Array) {
    
    
                    newObj[k]=[]
                    deepCopy(newObj[k], oldObj[k])
                } 
                //处理对象的问题
                else  if (oldObj[k] instanceof Object) {
    
    
                    newObj[k]={
    
    }
                    deepCopy(newObj[k], oldObj[k])
                } 
                else {
    
    
                    //k 属性名     oldObj[k] 属性值
                    newObj[k] = oldObj[k]   //到这儿只是浅拷贝,还需要判断数组这种复杂类型,所以加了一个if else
                }
            }
        }
JS库lodash里面cloneDeep内部实现了深拷贝

lodash是一个一致性,模块化,高性能的JavaScript实用工具库。

直接引入,使用

<script src="../js/lodash.min.js"></script>
    <script>
         const obj = {
            uname: 'pink',
            age: 18,
            hobby: ['羽毛球', '滑板',['111','111']],
            family:{
                baby:'小pink'
            }
        }
       const o= _.cloneDeep(obj)
       console.log(o);
       o.family.baby="老pink"
       console.log(obj);
    </script>
通过JSON.stringfy()实现深拷贝
 <script>
           const obj = {
      
      
            uname: 'pink',
            age: 18,
            hobby: ['羽毛球', '滑板',['111','111']],
            family:{
      
      
                baby:'小pink'
            }
        }
        // JSON.stringify()//把对象转化为JSON 字符串, 打印出来的是一堆字符串
        // JSON.parse()//在把字符串转化为对象
        const o=JSON.parse(JSON.stringify(obj))
       console.log(o);
       o.family.baby="老pink"
       console.log(obj);
    </script>

异常处理

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

1.throw抛异常

    function fn(x,y){
    
    
            if(!x||!y){
    
    
                //throw '没有参数传递进来'
                throw new Error('没有参数传递过来')
            }
            return x+y
        }
        console.log(fn());

总结:

  1. throw抛出异常信息,程序也会终止执行
  2. throw后面跟着的是错误提示信息
  3. Error对象配合throw使用,能够设置更详细的错误信息

2.try/catch捕获异常

我们可以通过try/catch捕获错误信息(浏览器提供的错误信息) try 试试 catch 拦住 finally 最后

<p>123</p>
    <script>
        function fn() {
      
      
            try {
      
      
                //可能发生错误的代码 要写到try里面
                const p = document.querySelector('.p')
                p.style.color = 'red'
            } catch (err) {
      
      
            //拦截错误,提示浏览器提供的错误信息,但是不中断程序的进程

                console.log(err.message);
                 throw new Error ('你看看,选择器错误了吧') //如果觉得浏览器提示的错误不够明显,可以搭配throw使用
                //想要中断程序,加上return
                // return
            }
            finally{
      
      
                //不管程序对不对,一定会执行的代码
                    alert('我无条件的执行')
                }
            console.log(11);
        }
        fn()
    </script>

总结:

  1. try/catch用于捕获错误信息
  2. 将预估可能发生错误的代码写在try代码段中
  3. 如果try代码段中出现错误后,会执行catch代码段,并截获到错误信息
  4. finally不管是否有错误,都会执行

3.debugger

与断点一样,不过会直接跳到断点处

处理this

this是JavaScript最具“魅惑”的知识点,不同的应用场合this的取值可能会有意向不到的结果。

this指向

普通函数this指向

普通函数的调用方式决定了this的值,即**【谁调用this的值指向谁】**

普通函数没有明确调用者时this值为window,严格模式(‘use strict’)下没有调用者时this的值为undefined。

    <button>点击</button>
    <script>
        //普通函数: 谁调用我,this就指向谁
        console.log(this);     //window
        function fn(){
      
      
            console.log(this);     //window
        }
        window.fn()
        window.setTimeout(function(){
      
      console.log(this);},1000)
        document.querySelector('button').addEventListener('click',function(){
      
      
            console.log(this);//指向button
        })
        const obj ={
      
      
            sayHi:function(){
      
      
                console.log(this);//指向obj
            }
        }
        obj.sayHi()
    </script>
箭头函数this指向

箭头函数中的this与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在this

像原型和构造函数中尽量不要使用箭头函数,因为原型和构造函数中要用到大量的this指向实例对象

  1. 箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的
  2. 箭头函数中的this引用的就是最近作用域中的this
  3. 向外层作用域中,一层一层查找this,直到有this的定义

总结:

  1. 函数内不存在this,沿用上一级的。向外层作用域中,一层一层查找this,直到有this的定义
  2. 不适用场景: 构造函数,原型函数,DOM事件函数等等
  3. 适用场景: 需要使用上层this的地方

改变this

JavaScript中还允许指定函数中this的指向,有3个方法可以动态指定普通函数中this的指向

1.call() (了解即可,使用场景较少)

使用call方法调用函数,同时指定被调用函数中this的值

     const obj={
    
    
            uname:"pink"
        }
        function fn(x,y){
    
    
            console.log(this);
        }
        //1.调用函数
        //2.改变this指向
        //fn()
        fn.call(obj,1,2)
2.apply()

使用apply方法调用函数,同时指定被调用函数中this的值

使用场景:求数组最大值

            const obj={
    
    
            age:18
        }
        function fn(x,y){
    
    
            console.log(this);
            console.log(x+y);
        }
        //1.调用函数
        //2.改变this指向
        // fn.apply(this指向谁,数组参数)
        fn.apply(obj,[1,2])
        //3.返回值 本身就是在调用函数,所以返回值就是函数的返回值
        //使用场景: 求数组最大值
        // const max=Math.max(1,2,3)
        // console.log(max);  //之前的写法
        const arr=[100,2,22]
        const max=Math.max.apply(Math,arr)
        const min=Math.min.apply(Math,arr)
        console.log(max,min);  

**小结:**求数组最大值(已有至少三种):

  1. 遍历数组求最大值
  2. 扩展运算符
  3. apply方法
3.bind()

bind方法不会调用函数,但是能改变函数内部的this指向

因此当我们只想改变this指向,并且不想调用这个函数的时候,可以使用bind,比如改变定时器内部的this指向

    <button>点击</button>
    <script>
          const obj={
      
      
            age:18
        }
        function fn(){
      
      
            console.log(this);
        }
        //1.bind 不会调用函数
        //2.能改变this指向
        //3.返回值是个函数, 但是这个函数里面的this是更改过的obj
        const fun=fn.bind(obj)
        console.log(fun);
        fun()
        const btn=document.querySelector('button')
        btn.addEventListener('click',function(){
      
      
            //禁用按钮
            this.disabled=true
            window.setTimeout(function(){
      
      
                //在这个普通函数里面,我们要this由原来的的window改为btn
                this.disabled=false
            }.bind(btn),2000)   //这里写this是调用上一级setTimeout的window  用bind解决问题
        })
        //需求:有一个按钮,点击里面就禁用,2秒之后开启
    </script>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P7YKmk7T-1681567960123)(C:\Users\86184\Desktop\4.3-4.14\call,apply,bind.png)]

性能优化

防抖(debounce)

单位时间内,频繁触发事件,只执行最后一次

使用场景:

搜索框搜索使用。只需用户最后一次输入完,再发送请求。

手机号,邮箱验证输入检测

做个小案例,不加防抖,鼠标滑动,盒子内的数加1。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box{
      
      
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    </style>
</head>
<body>
    <div class="box"></div>
    <script>
        //需求:鼠标在盒子上移动,里面的数字就会变化+1
        const box=document.querySelector('.box')
        let i=1
        function mouseMove(){
      
      
            box.innerHTML=i++
            //如果里面存在大量消耗性能的代码,就会使性能大大降低
        }
        //添加事件
        box.addEventListener('mousemove',mouseMove)
    </script>
</body>
</html>

加上防抖:鼠标在盒子上移动,鼠标停止500ms之后,里面的数字才会变化+1

实现方式(两种):

1.lodash提供的防抖处理

2.手写一个防抖函数来处理(核心思路:利用定时器setTimeout来实现)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box{
      
      
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    </style>
</head>
<body>
    <div class="box"></div>
    <script src="../js/lodash.min.js"></script>
    <script>
        //利用防抖实现性能优化
        //需求:鼠标在盒子上移动,里面的数字就会变化+1
        const box=document.querySelector('.box')
        let i=1
        function mouseMove(){
      
      
            box.innerHTML=i++
            //如果里面存在大量消耗性能的代码,就会使性能大大降低
        }
        // //添加事件
        // box.addEventListener('mousemove',mouseMove)

        //方法一.利用Lodash库实现防抖 -500ms之后采取+1
        //语法:_.debounce(fun,时间)
        // box.addEventListener('mousemove',_.debounce(mouseMove,500))
        
        //方法二.手写一个防抖函数来处理(核心思路:利用定时器setTimeout来实现)
        //1.声明定时器变量
        //2.每次鼠标移动(事件触发)的时候都要先判断是否有定时器,如果有先清除之前的定时器
        //3.如果没有定时器,则开启定时器,存入到定时器变量中
        //4.定时器里面写函数调用
        function debounce(fn,t){
      
      
            let timer
            //return 返回一个匿名函数
            return function(){
      
      
                if(timer) clearTimeout(timer)
                timer =setTimeout(function(){
      
      
                    fn()  //加小括号调用fn函数
                },t)
            }
        }
        box.addEventListener('mousemove',debounce(mouseMove,500))
    </script>
</body>
</html>

节流(throttle)

单位时间内,频繁触发事件,只执行一次

使用场景:

高频事件:鼠标移动mousemove、页面尺寸缩放resize、滚动条滚动scroll等等

加上节流:鼠标在盒子上移动,不管移动多少次,每隔500ms里面的数字才会变化+1

实现方式(两种):

1.lodash提供的节流处理

2.手写一个节流函数来处理(核心思路:利用定时器setTimeout来实现)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
      
      
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    </style>
</head>

<body>
    <div class="box"></div>
    <script src="../js/lodash.min.js"></script>
    <script>
        //利用节流实现性能优化
        //需求:鼠标在盒子上移动,不管移动多少次,每隔500ms里面的数字才会变化+1
        const box = document.querySelector('.box')
        let i = 1
        function mouseMove() {
      
      
            box.innerHTML = i++
            //如果里面存在大量消耗性能的代码,就会使性能大大降低
        }
        // //添加事件
        // box.addEventListener('mousemove',mouseMove)

        //方法一.利用Lodash库实现防抖 -500ms之后采取+1
        //语法:_.throttle(fun,时间)
        // box.addEventListener('mousemove',_.throttle(mouseMove,3000))

        // 方法二.手写一个节流函数来处理(核心思路:利用定时器setTimeout来实现)
        // 1.声明定时器变量
        // 2.每次鼠标移动(事件触发)的时候都要先判断是否有定时器,如果有先清除之前的定时器
        // 3.如果没有定时器,则开启定时器,存入到定时器变量中
        // 4.定时器里面写函数调用
        function throttle(fn, t) {
      
      
            let timer = null
            //return 返回一个匿名函数
            return function () {
      
      
                if (!timer) {
      
      
                    timer = setTimeout(function () {
      
      
                        fn()  //加小括号调用fn函数
                        //清空定时器
                        timer=null
                    }, t)
                }
            }
        }
        box.addEventListener('mousemove', throttle(mouseMove, 3000))
    </script>
</body>

</html>

清楚定时器问题

 let timer =null
        timer =setTimeout(()=>{
    
    
            clearTimeout(timer)
            console.log(timer);//结果为1
        },1000)
//在setTimeout中是无法删除定时器的,因为定时器还在运作,所以使用timer=null  而不是clearTimeout(timer)
    

执行上下文栈

JavaScript引擎执行是顺序执行的,但是它并不是一行一行地分析和执行代码,而是一段一段地分析和执行的。当执行每一段代码之前,他会先做一个“准备工作”。

更专业的说法就是创建“执行上下文(execution context)”

例如:变量声明的提升,函数声明的整体提升等

**问题:**我们写的函数很多,JavaScript引擎如何创建和管理这么多执行上下文呢?

JavaScript引擎通过创建执行上下文栈(Execution context stack,ECS)来管理执行上下文。

当JavaScript开始解释执行代码的时候,最先遇到的就是全局代码,所以它会向执行上下文栈压入一个全局执行上下文,我们用globalContext表示,并且只有当整个程序运行结束时,ECStack才会被清空,所以程序结束之前,ECStack最底部永远有个globalContext。

当JavaScript执行一个函数时,它就会创建一个执行上下文,并且压入执行上下文栈。当函数执行完毕时,就会将函数的执行上下文从栈里弹出。

进程与线程

进程(process)

程序的一次执行,它占有一片独立的内存空间

可以通过Windows任务管理器查看进程

线程(thread)

是进程内一个独立执行单元

是程序执行的一个完整流程

是CPU的最小调度单元

图解

在这里插入图片描述

相关知识

  1. 应用程序必须运行在某个进程的某个线程上
  2. 一个进程中至少有一个运行的线程:主线程,进程启动后自动创建
  3. 一个进程中也可以同时运行多个线程,我们会说程序是多线程运行的
  4. 一个进程内的数据可以供其中的多个线程直接共享
  5. 多个线程之间的数据不能直接共享
  6. 线程池(thread pool)保存多个线程对象的容器,实现线程对象的反复利用

问题:

1.何为多进程与多线程?

多进程运行:一应用程序可以同时启动个实例运行

多线程:在一个进程内,同时有多个线程运行

2.比较单线程与多线程

多线程

优点:能够有效提升CPU的利用率

缺点:创建多线程开销、线程间切换开销、死锁与状态同步问题

单线程

优点:顺序编程简单易懂

缺点:效率低

3.JS是单线程运行的,但使用H5中的Web Worker是可以多线程运行
4.浏览器运行是多线程的

浏览器有单进程(firefox、老版IE)

也有多进程(Chrome 、新版IE)

猜你喜欢

转载自blog.csdn.net/L19541216/article/details/130176277