一、深浅拷贝
-
❗❗ 面试重点
-
开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题:
- 变量之间相互赋值的时候,传递的是栈中的值
- 复杂数据类型 栈中存放的是数据地址,真正的数据存放在堆里面
-
浅拷贝 和 深拷贝 只针对 引用类型
1.1 浅拷贝
- 浅拷贝: 拷贝的是 地址
- 常见方法:
- 拷贝对象:
- 1️⃣
Object.assgin()
或者{...obj}
- 2️⃣ 使用
for - in
遍历原对象,逐个拷贝属性和属性值
- 1️⃣
- 拷贝数组:
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 和 函数
- 语法:
- 1️⃣ 通过 函数递归 实现深拷贝
二、异常处理
- 提升代码的健壮性
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,会自动使用 基本包装数据类型 把 简单数据 转换为 复杂数据类型
- thisArg:在函数运行时指定
- 返回值: 函数的返回值,因为它就是在调用函数
- 特点:
- 会 立即调用 函数(不适合用作 定时器处理函数 和 事件处理函数 )
- 作用:
- 调用函数
- 改变 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
指向
- 都可以改变函数内部的
- 区别点:
call
和apply
会 立即调用函数,并且改变函数内部的this
指向call
和apply
传递的 参数不一样,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完成过渡后触发