前端 - 笔记 - JavaScript - 高级【深浅拷贝 + 异常处理(throw、try-catch) + this指向 + 性能优化(防抖 + 节流)】

一、深浅拷贝

  • ❗❗ 面试重点

  • 开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题:

    • 变量之间相互赋值的时候,传递的是栈中的值
    • 复杂数据类型 栈中存放的是数据地址,真正的数据存放在堆里面在这里插入图片描述
  • 浅拷贝深拷贝 只针对 引用类型

1.1 浅拷贝

  • 浅拷贝: 拷贝的是 地址
  • 常见方法:
    • 拷贝对象:
      • 1️⃣ Object.assgin() 或者 {...obj}
      • 2️⃣ 使用 for - in 遍历原对象,逐个拷贝属性和属性值
    • 拷贝数组:
      • Array.prototype.concat() 或者 [...arr]
      • 遍历原数组,逐个拷贝元素
  • 浅拷贝存在的问题:
    • 只能拷贝 一层 对象或数组
  • 代码展示:
    // 拷贝对象
    const pink = {
      uname: 'pink老师',
      age: 18
    };
    // 1.展开运算符 - 将pink的属性展开从新加入到这个新对象里
    // const copyPink = { ...pink };
    // 2.静态方法-拷贝对象
    const copyPink = {};
    Object.assign(copyPink, pink);
    console.log(copyPink);  // {uname: 'pink老师', age: 18}
    copyPink.uname = '刘德华';
    console.log(copyPink);  // {uname: '刘德华', age: 18}
    console.log(pink);      // {uname: 'pink老师', age: 18}
    
    // 拷贝数组
    const arr = ['迪迦', '赛罗'];
    // 1.展开运算符
    // const copyArr = [...arr];
    // 2.合并数组的方法
    const copyArr = Array.prototype.concat(arr);
    console.log(copyArr); // ['迪迦', '赛罗']
    copyArr[0] = '迪迦奥特曼';
    console.log(copyArr); // ['迪迦奥特曼', '赛罗']
    console.log(arr);     // ['迪迦', '赛罗']
    
    // 问题
    const obj1 = {
      uname: 'pink老师',
      age: 18,
      family: {
        baby: '小pink'
      }
    };
    const obj2 = { ...obj1 };
    const obj3 = {};
    Object.assign(obj3, obj1);
    obj2.family.baby = '老pink';
    console.log(obj 1, obj2, obj3);
    // {uname: 'pink老师', age: 18, family: { baby: '老pink' }}
    // {uname: 'pink老师', age: 18, family: { baby: '老pink' }}
    // {uname: 'pink老师', age: 18, family: { baby: '老pink' }} 
    
  • 直接赋值 和 浅拷贝 有什么区别?
    • 直接赋值:只要是对象,都会相互影响,因为是将栈里面的地址赋值给变量
    • 浅拷贝:如果只拷贝一层对象,不相互影响,如果出现多层对象拷贝还是会相会影响
  • 浅拷贝怎么理解?
    • 拷贝对象之后,里面的 属性值 是 简单数据类型 直接 拷贝值
    • 如果 属性值 是 引用数据类型 则 拷贝 地址

1.2 ❗❗ 深拷贝

  • 深拷贝: 拷贝的是 对象 ,不是地址
  • 🔺🔺❗❗❗面试必考题
  • 常用方法:
    • 1️⃣ 通过 函数递归 实现深拷贝
      • 递归函数介绍: 如果一个函数在 内部 可以 调用自身,那么这个函数就是递归函数
        • 函数内部自己调用自己

        • 递归函数的作用和循环类似

        • ⚠ 由于 递归 很容易 发生 “栈溢出” 错误,所以 必须加 退出条件 return

        • 递归嵌套太深出现以下错误
          Uncaught RangeError: Maximum call stack size exceeded
          超出最大调用堆栈大小
          

          在这里插入图片描述

        • 代码展示:

          // 利用递归函数实现 setTimeout 模拟 setInterval效果
          function fn() {
            console.log(new Date());
            setTimeout(fn, 1000);
          }
          fn();
          
      • 实现深拷贝:
        const oldObj = {
          uname: '迪迦',
          age: 22,
          hobby: ['打怪兽', '晒日光浴', '揍银河'],
          family: {
            wife: '卡尔蜜拉',
            baby: '小迪迦'
          }
        };
        
        const newObj = {};
        // 使用递归函数实现深拷贝
        function deepCopy(newObj, oldObj) {
          // 1.遍历对象
          for (let k in oldObj) {
            // 1.处理 属性值 = 数组
            if (oldObj[k] instanceof Array) {
              newObj[k] = [];
              deepCopy(newObj[k], oldObj[k]);
            }
            // 2.处理 属性值 = 对象
            else if (oldObj[k] instanceof Object) {
              newObj[k] = {};
              deepCopy(newObj[k], oldObj[k]);
            }
            // 3.对于简单数据类型直接进行赋值操作
            // 新对象.旧对象属性名 = 旧对象属性值
            else {
              newObj[k] = oldObj[k];
            }
          }
        }
        deepCopy(newObj, oldObj);
        newObj.family.twoWife = '丽娜';
        console.log('旧对象:', oldObj);
        console.log('新对象:', newObj);
        // 旧对象:
        // {
        //   uname: '迪迦',
        //   age: 22,
        //   hobby: ['打怪兽', '晒日光浴', '揍银河'],
        //   family: {wife: '卡尔蜜拉', baby: '小迪迦'}
        // }
        // 新对象:
        // {
        //   uname: '迪迦',
        //   age: 22,
        //   hobby: ['打怪兽', '晒日光浴', '揍银河'],
        //   family: {wife: '卡尔蜜拉', baby: '小迪迦', twoWife: '丽娜'}
        // }
        
      • ❗❗ 面试总结: 用递归函数实现过深拷贝吗?
        • 1️⃣ 深拷贝:新对象不会影响旧对象
        • 2️⃣ 实现深拷贝需要用到函数递归
        • 3️⃣ 普通拷贝直接进行赋值操作
          • 如果遇到数组,再次调用递归函数解决数组
          • 如果遇到函数,直接进行赋值操作(函数就是为了实现代码的复用)
          • 如果遇到对象,再次调用递归函数解决对象
          • ⚠ 先解决数组或函数 再解决对象(数组和函数也属于对象)
    • 2️⃣ lodash里面的cloneDeep (JS库)
      • 使用之前要先引入 lodash.js
      • 语法: const newArr / newObj = _.cloneDeep(oldArr / oldObj)
      • 返回值:拷贝完的对象
    • 3️⃣ 通过 JSON.stringify() 实现
      • 语法:
        JSON.parse(JSON.stringify(Obj))
        
      • 原理:
        • 1️⃣ 将 对象 转换成 JSON字符串 ,基本数据类型
        • 2️⃣ 然后再转换为复杂数据类型,再从堆里面开辟一个新的空间,前后两个对象没有任何关系
      • 缺点: (面试)
        • 并不能转换所有的值
        • 不识别 undefined 和 函数

二、异常处理

  • 提升代码的健壮性

2.1 throw抛异常

  • 基本语法:
    throw new Error('要抛出的信息')
    
  • 总结:
    • throw 抛出异常信息,程序也会 终止执行
    • throw后面跟的是错误提示信息
    • Error对象配合throw使用,能够设置更详细的错误信息

2.2 try-catch 捕获异常

  • 捕获错误信息(浏览器提供的)
    • try - 尝试(可能发生错误的代码)
    • catch - 拦住
    • finally - 最后(不管程序是否正确,一定会执行的里面的代码)
    • ⚠ 不会自动中断程序(想要中断需要加return)
  • 总结:
    • try-catch用于捕获错误信息
    • 将预估可能发生错误的代码写在try代码段中
    • 如果try代码段中出现错误后,会执行catch代码段,并截取到错误信息
    • finally不管是否有错误,都会执行
  • 代码展示:
    // 不一定要写在函数里面
    function fn() {
      try {
        const body = document.body;
        body.style.backgroundColor = 'purple';
      } catch(err) {
        // 拦截错误,提示浏览器提供的错误信息,但是不中断程序
        // message:必须写(属性:错误的详细信息),err是自定义的
        // console.log(err.message);
        // 要想中断程序必须加return
        // return
    
        // 还不如直接写throw
        throw new Error('选择器错误');
      } finally {
        alert('不管程序是否正确,一定会执行的代码');
      }
    }
    fn();
    

2.3 debugger

  • 类似与打断点
  • 调式的时候使用
  • 使用:直接添加到代码对应位置

三、处理this

3.1 this指向

  • 普通函数this
    • 谁调用指向谁
    • 🔺 独立的普通函数(不归属于任何对象)this指向window
      • 代码展示:
        let obj = {
          name: '阿茶',
          hi: function() {
            console.log(this);	// this -> obj
            let fun = () => {
              console.log(this);	// this -> obj
              let fn = () => {
                console.log(this);	// this -> obj
                let count = function () {
                  console.log(this);	// this -> window
                  let amount = () => {
                    console.log(this);	// this -> window
                  }
                  amount();
                }
                count();
              }
              fn();
            }
            fun();
          }
        };
        obj.hi();
        
    • 普通函数 没有 明确调用者 时this指向window
    • ❌ 严格模式下没有调用者时this指向undefined,严格模式写在当前作用域或全局作用域的最前面 ('user strict'
  • 箭头函数的this
    • 箭头函数中的this与普通函数完全不同,也不受调用方式的影响,箭头函数不存在this
    • 箭头函数自己不会创建this,它只会沿用本体所在作用域的上层作用域的this
    • 不适用: 构造函数,原型函数,dom事件函数
      定义: this 是一个使用在 作用域 内部的 关键字(包括函数作用域 和 全局作用域)

3.2 ❗❗ 改变this

  • 1️⃣ call()
    • 使用call方法调用函数,同时 改变 被调用函数 中this的值
    • 语法:
      函数名.call(thisArg, arg1, arg2, ...)
      
    • 参数:
      • thisArg:在函数运行时指定this
      • arg1, arg2:函数的实参
      • ⚠ 注意:如果写入一个简单数据类型替换this,会自动使用 基本包装数据类型简单数据 转换为 复杂数据类型
    • 返回值: 函数的返回值,因为它就是在调用函数
    • 特点:
      • 立即调用 函数(不适合用作 定时器处理函数 和 事件处理函数 )
    • 作用:
      • 调用函数
      • 改变 this 指向
    • 应用场景: 伪数组借用数组方法
    • 代码展示:⬇
  • 2️⃣ ✔ apply()
    • 语法:
      函数名.apply(thisArg[, argsArray]);
      
    • 参数:
      • thisarg:在函数运行时指定的this的值(this指向谁,不改变指向使用null
      • argsArray:函数的实参,必须包含在 数组 里面
    • 返回值: 函数的返回值,因为它就是在调用函数
    • 特点:
      • 立即调用 函数(不适合用作 定时器处理函数 和 事件处理函数 )
    • 作用: 可以以数组的形式给某些 功能函数 传参
    • 使用场景: 求数组的最值(1.for循环遍历 2.展开运算符 3.apply)
    • 代码展示:⬇
      const obj = {uname: '迪迦', age: 22};
      function fun(x, y) { 
        console.log(this); 
        console.log(x + y);
      }
      fun.apply(obj, [3, 4]);	// obj, 7
      
      const max = Math.max.apply(Math, [34, 65, 12, 30]);	// 65
      const min = Math.min.apply(Math, [34, 65, 12, 30]);	// 12
      
    • call 和 apply 的区别:
      • 都是调用函数,都能改变this指向
      • 参数不一样
        • call 传递的是多个值
        • apply 是以数组的形式给函数传递参数
  • 3️⃣ ✔🔺bind()
    • bind()方法 不会调用函数 ,但是能改变函数内部this指向
    • 语法:
      函数名.bind(thisArg, arg1, arg2, ...)
      
    • 参数:
      • thisArg:this指向谁
      • arg1, arg2, …:给函数传递参数
    • 返回值:
      • 由指定的this值和初始化参数改造的 原函数拷贝(新函数)
      • 会返回一个新的函数,一个已经改变好 this 指向的函数
    • 特点: 不会调用函数
    • 只想改变this指向,并不想立即调用函数的时候,可以使用bind(定时器内部的this,注册事件)
    • 代码展示:⬇
  • 代码展示:
    const obj = { uname: '迪迦', age: 22 };
    function fun(x, y) {
      console.log(this);
      console.log(x + y);
    }
    // 1、call()方法:
    // 语法:fun.call(thisArg[, arg1[, arg2 [, ...]]]);
    // 参数:
    //  1)要指向谁
    //  2)多个参数,用来给函数传递实参
    // 返回值:函数的返回值(call本质上就是在调用函数)
    fun.call(obj, 3, 4);  // obj, 7
    
    // 2.apply()方法:
    // 语法:fun.apply(thisArg[, [arg1, arg2, ...]])
    // 参数:
    //  thisArg:this指向谁
    //  第二个参数是 数组 ,用来给函数传递实参
    // 返回值:函数的返回值(apply本质上就是在调用函数)
    // 使用场景:求数组的最值 (1:for循环遍历 2:展开运算符 3:apply)
    fun.apply(obj, [4, 5]); //  obj, 9
    // 最值
    const arr = [23, 67, 10, 89, 20];
    console.log(Math.max.apply(Math, arr)); // 89
    console.log(Math.min.apply(Math, arr)); // 10
    
    // 3.bind()方法:
    // 语法:fun.bind(thisArg, arg1, arg2, ...)
    // 参数:
    //  thisArg:this指向谁
    //  arg1, arg2:用来给函数传递参数
    // 返回值:一个已经改变好this指向的函数(和原函数一样,除了this指向)
    // 特点:不立即调用函数
    const fn = fun.bind(obj, 4, 6);
    console.log(fn);
    fn();
    // bind应用
    const btn = document.querySelector('button');
    btn.addEventListener('click', function () {
      this.disabled = true;
      setTimeout(function () {
        this.disabled = false;
      }.bind(this), 2000);
    });
    

3.3 小结

  • 相同点:
    • 都可以改变函数内部的 this 指向
  • 区别点:
    • callapply立即调用函数,并且改变函数内部的this指向
    • callapply 传递的 参数不一样call传递参数 arg1, arg2, ...形式,apply 必须数组 形式 [arg]
    • bind 不调用函数,可以改变函数内部的this指向
  • 主要应用场景:
    • call 调用函数 并且可以传递参数
    • apply 经常跟 数组 有关,比如借助于数学对象实现数组最值
    • bind 不调用函数,但是还想改变this指向,比如改变 定时器内部的this指向

四、性能优化

4.1 防抖

  • 防抖(debounce)
    • 事件发生一段时间再执行,如果这段时间内继续触发新的事件,那么取消之前的事件,只执行最新的事件;
    • 延迟执行;
  • 使用场景:
    • 搜索框(定时器)
    • 代码展示:
      <input type="text">
      <script>
          const input = document.querySelector('input');
          let timerId = null;
          input.addEventListener('input', () => {
              // 先清除之前的定时器
              clearTimeout(timerId);
              // 在开启当前的定时器
              timerId = setTimeout(() => {
                  console.log(11);
              }, 500);
          });
      </script>
      

4.2 节流

  • 节流(throttle):
    • 一段时间内只执行一次事件,执行结束后才能继续执行新的事件。(限制任务执行频率的一种手段);
    • 节流是一种手段,来限制任务执行频率,规则是当前任务执行结束之前,不接受新任务,直到当前任务结束,才开始执行下一个任务;
  • 使用场景:
    • 轮播图点击效果、鼠标移动、页面尺寸缩放(resize)、滚动条滚动
  • 实现节流:
    • 起始时间 - 当前时间
  • 代码展示:
    <style>
      * {
         margin: 0;
         padding: 0;
       }
    
      .box {
         width: 100px;
         height: 100px;
         background-color: red;
         margin-top: 30px;
         transition: all 1s linear;
       }
    </style>
    
    <body>
      <button>按钮</button>
      <div class="box"></div>
      <script>
      const btn = document.querySelector('button');
          const box = btn.nextElementSibling;
          // 声明变量:作为box的初始宽度
          let width = 100;
          // 声明变量:节流阀
          let flag = true;
          // 事件侦听
          btn.addEventListener('click', () => {
              if (flag) {
                  flag = false;
                  width += 100;
                  box.style.width = width + 'px';
              }
          });
          // 拓展:transitionend - CSS过渡完触发事件
          box.addEventListener('transitionend', () => {
              flag = true;
          });
      </script>
    </body>
    
  • ❗❗❗ 面试 节流 和 防抖 的区别?
    • 节流: 一段时间内只执行一次事件,执行结束后才能继续执行新的事件。(限制任务执行频率的一种手段);
      • 是一种手段,用来限制任务的执行频率,规则是当前任务结束之前,不会接收新任务,直到当前任务结束,才会执行新任务
      • 采用两个时间(事件开始执行时间 - 事件执行结束时间)相减的方式实现,如果相减结果大与指定时间就调用函数
    • 防抖: 事件发生一段时间再执行,如果这段时间内继续触发新的事件,那么取消之前的事件,只执行最新的事件;
      • 定时器方式实现:在事件触发过程中一直清除定时器,当事件在一定时间内不触发就调用函数
    • 使用场景:
      • 节流:轮播图点击切换按钮、鼠标移动、页面尺寸缩放、滚动条滚动
      • 防抖:搜索框
  • 案例拓展:
    • timeupdate 事件在 视频 / 音频 当前的播放位置 发生改变 时触发
    • loadeddata 事件在 当前帧的数据加载完成 且还 没有足够的数据 播放视频 / 音频的下一帧 时触发
    • currentTime 可读写 属性:获取 当前 视频 / 音频时间
    • transitionend 事件在CSS完成过渡后触发
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值