本文介绍JS中的一些高阶技巧。
目录
1. 深浅拷贝
开发中经常需要复制一个对象,如果直接复制会有以下问题:
<script>
const obj = {
uname : 'zzz',
age : 18
}
const o = obj;
console.log(o);
// 修改o
o.age = 20;
console.log(o);
console.log(obj); // 20 obj的值也被修改了
</script>
给o的复制相当于把地址给了o,修改后对obj也修改了
1.1 浅拷贝
首先深浅拷贝只针对引用类型
浅拷贝拷贝的是地址
常见方法:
<script>
const obj = {
uname : 'zzz',
age : 18
}
const o = {...obj};
console.log(o);
o.age = 20;
console.log(o);
console.log(obj); // 这里没有被改变 仍是18
</script>
<script>
const obj = {
uname : 'zzz',
age : 18
}
const o = {}
Object.assign(o,obj);
o.age = 20;
console.log(o);
console.log(obj); // 没有被改变 18
</script>
但是只是对于简单的不会被改,比如下面的问题
<script>
const obj = {
uname : 'zzz',
age : 18,
family : {
baby : 'bbb'
}
}
const o = {}; //定义对象
Object.assign(o,obj);
o.family.baby = 'aaa';
console.log(o);
console.log(obj); // 这里的baby被修改了
</script>
内层的仍然被修改,内层拷贝的仍然是地址,只有外层的是只拷贝了值
所以是浅拷贝
1.2 深拷贝
只针对引用类型
深拷贝:拷贝的是对象 而非地址
常见方法:
1.2.1 通过递归实现
如果一个函数在内部可以调用其本身,就是递归函数
即 自己调用自己 类似循环
由于递归容易发生栈溢出错误,所以必须要加退出条件 return
<script>
let i = 1;
function fn(){
console.log(`这是第${i}次`)
if(i>=6){
return // 退出
}
i++;
fn(); // 调用自己 递归 容易死递归 栈溢出
}
fn(); // 外部先调用才能开始
</script>
利用递归函数实现 setTimeout 模拟 setInterval 效果
需求:
① 页面每隔一秒输出当前时间
② 输出时间用 new.Date().toLocaleString()
<body>
<div></div>
<script>
function getTime(){
document.querySelector('div').innerHTML = new Date().toLocaleString();
// 每隔一秒调用一次这个函数
setTimeout(getTime,1000);
}
getTime();
</script>
</body>
数组情况:
<script>
const obj = {
uname : 'zzz',
age : 18,
hobby: ['乒乓球','足球']
}
// 创建新对象
const o = {}
// 拷贝函数
// 利用遍历 把每个值取出复制
function deepCopy(newobj,oldobj){
// for in 方法遍历对象 旧obj
for(let k in oldobj){
// 处理数组问题
if(oldobj[k] instanceof Array){
// 遍历数组
// 递归调用自己来实现复制
// 先创建一个新数组
newobj[k] = [];
deepCopy(newobj[k],oldobj[k]); // 传入的参数是数组
}
// 非数组情况
else{
// k是属性名 oldobj[k]是属性值 这里只复制值
newobj[k] = oldobj[k];
// newobj[k] === o.uname
}
}
}
deepCopy(o,obj); // o新 obj旧
// 本质还是浅拷贝
o.hobby[0] = '篮球';
console.log(obj); // 此时没有被修改
</script>
在这里注意 newobj[k] 是[ ]存的变量k
如果对象里还有嵌套对象 可以仿照数组的形式
加上以下内容即可
// 处理对象问题
else if(oldobj[k] instanceof Object){
// 遍历对象
// 递归调用自己来实现复制
// 先创建一个新对象
newobj[k] = {};
deepCopy(newobj[k],oldobj[k]); // 传入的参数是数组
}
注意:
一定先写数组后写对象,因为数组也是对象,不然操作出现错误
1.2.2 lodash / cloneDeep
JS库的lodash里面cloneDeep
<body>
<!-- 先引用lodash库,这里仅做使用演示,实际没有文件 -->
<script src="./lodash.in.js"></script>
<script>
const obj = {
uname : 'zzz',
age : 18,
hobby: ['乒乓球','足球'],
family: {
baby:'zzz'
}
}
const o = _.cloneDeep(obj);
console.log(o);
o.family.baby = 'aa';
console.log(obj);
</script>
</body>
1.2.3 JSON.stringify()
<script>
const obj = {
uname : 'zzz',
age : 18,
hobby: ['乒乓球','足球'],
family: {
baby:'zzz'
}
}
// 将对象转为JSON字符串
// console.log(JSON.stringify(obj));
// 转为字符串后变为简单数据类型 和对象没有关系了
// 将JSON字符串转为新对象
const o = JSON.parse(JSON.stringify(obj));
console.log(o);
</script>
对象 —— 字符串 —— 新对象
新对象和之前的对象没有任何关系
2. 异常处理
了解JS中程序异常处理的办法 提升代码运行的健壮性
2.1 throw 抛异常
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
<script>
function fn(x,y){
if(!x || !y){
// console.log(11); // 没有传值
// throw '没有参数传递';
// throw会中断程序运行
// 一般搭配 new Error 使用
throw new Error('没有参数传递过来');
}
return x+y;
}
console.log(fn());
</script>
2.2 try / catch 捕获异常
try先判断是否有错,有错就 catch 拦住 finally 最后执行什么
<body>
<p>123</p>
<script>
function fn(){
// 将容易犯错的部分写到try部分
try{
const p = document.querySelector('.p');
p.style.color = 'red';
} catch(err) {
// err保存浏览器提供的错误信息
console.log(err.message); // message是信息
// 前面的标签不能有.
// 报错信息:Cannot read properties of null (reading 'style')
return
// 退出
}
finally {
// 不管对不对都会执行的代码
alert('zzzz');
}
console.log(11);
// 没有return时 能输出 说明try catch不会中断 需要return 或者搭配throw使用
}
fn();
</script>
</body>
2.3 debugger
在某位置加上debugger后
打开Sources 刷新后自动跳到debugger位置
3. 处理this
3.1 this指向
3.1.1 普通函数this
普通函数的调用方式决定了this的值,即谁调用this的值就指向谁
<body>
<button>点击</button>
<script>
console.log(this); // window
function fn(){
console.log(this); // window
}
fn();
setTimeout(function(){
console.log(this);
},1000); // window
document.querySelector('button').addEventListener('click',function(){
console.log(this); // button
})
const obj = {
sayHi : function(){
console.log(this); // obj
}
}
obj.sayHi();
</script>
</body>
3.1.2 箭头函数的this
箭头函数于普通函数完全不同,也不受调用方式影响,事实上箭头函数并不存在this
1. 箭头函数会默认绑定外层的this,所以箭头函数的this和外层的this一样
2. 箭头函数的this引用的时最近作用域的this
3. 向外层的作用域中,一层一层查找this,直到有this的定义
注意不推荐的:
1. DOM事件中如果需要DOM对象里的this 不推荐使用箭头函数
2. 原型对象不推荐使用箭头函数 否则this不指向实例对象了
3.2 改变this
3.2.1 call方法改变
call调用函数,同时指定被调用函数中的this的值
语法:
fun.call(thisArg, arg1, arg2...)
thisArg:在fun函数运行时指定的this值
arg1 arg2:传递的其他参数
返回值就是函数的返回值,因为它是调用函数
<script>
const obj = {
uname : 'zzz'
}
function fn(x,y){
console.log(this); // window
console.log(x+y);
}
// 1.调用函数
// 2.改变this指向
fn.call(obj,1,2); // 指向window 变为 指向obj
// x给1,y给2 代表其他操作
</script>
3.2.2 apply方法改变
apply调用函数,同时指定被调用函数中的this的值
语法:
fun.call(thisArg, [argsArray])
thisArg:在fun函数运行时指定的this值
argsArray:传递的其他参数必须包含在数组内
返回值就是函数的返回值,因为它是调用函数
apply主要和数组有关,比如调用Math.max()求数组最大值
<script>
const obj = {
uname : 'zzz'
}
function fn(...Array){
console.log(this);
console.log(Array);
}
// 1.调用函数 2.改变this指向
fn.apply(obj,[2,3,1,6,19,11]);
// 返回值 本身就是调用函数 返回值就是函数返回值
// 求数组最值
const max = Math.max.apply(Math,[1,2,3,5]) // 首先指向Math
const min = Math.min.apply(Math,[1,2,3,5]) // 首先指向Math
console.log(max,min);
arr = [1,2,3,4];
console.log(Math.max(...arr));
</script>
3.2.3 bind方法改变
bind() 不会调用函数 但是可以改变内部this指向
语法:
fun.bind(thisArg, arg1, arg2...)
thisArg:在fun函数运行时指定的this值
arg1 arg2:传递的其他参数
返回由指定的this值和初始化参数改造的原函数拷贝(新函数)
只想改变this指向 但是不想调用函数的时候bind()
<script>
const obj = {
uname : 'zzz'
}
function fn(){
console.log(this);
}
// bind不会调用函数
// 能改变this指向
// 返回的是个函数 但是里面的this是改后的
const fun = fn.bind(obj);
// console.log(fun);
fun();
</script>
改变定时器内部的 this 指向
<body>
<button>发送</button>
<script>
// 需求:有一个按钮,点击就禁用,2s后开启
const btn = document.querySelector('button');
btn.addEventListener('click',function(){
this.disabled = true // 禁用吗?true this是按钮
setTimeout(function(){ // window调用了计时器
// 普通函数中由原来的window改为btn
this.disabled = false; // btn开启
}.bind(this),2000) // this指向btn 和上面的this指向的都是btn 所以this即可
})
</script>
</body>
4. 防抖 debounce
单位时间内,频繁触发事件,只执行最后一次。
使用场景:
搜索框搜索输入字符时,只需要用户输完最后一个字,再发送搜索请求。
例如:
利用防抖处理-鼠标滑过盒子就显示文字
鼠标在盒子上移动,里面的数字会变化+1
方法:
4.1 lodash
_.debounce(func, [wait = 0], [options=])
该函数会从上一次被调用后,延迟wait毫秒后调用func方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box{
width: 300px;
height: 500px;
background-color: #ccc;
color: #fff;
text-align:center;
font-size: 100px;;
}
</style>
</head>
<body>
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
// 防抖实现性能优化
// 需求:鼠标在盒子上移动 里面的数字就变化+1
// 首先获取盒子
const box = document.querySelector('.box')
let i = 1; // 设置初始
function mouseMove(){
box.innerHTML = i++;
// 如果存在大量消耗性能的代码 如DOM 数据处理等 可能造成卡顿
// 鼠标在盒子上停留500ms后 里面数字再加一
}
// 添加滑过事件
// box.addEventListener('mousemove',mouseMove)
// 1.lodash库实现防抖 500ms后+1
// box.addEventListener('mousemove',_.debounce(mouseMove,500));
</script>
</body>
</html>
4.2 手写防抖
防抖核心:setTimeout实现
① 声明一个计时器变量
② 当鼠标每次滑动都先判断是否有定时器了,如果有定时器先清除以前的定时器
③ 如果没有以前的定时器就开启定时器,存到变量里
④ 定时器里调用要执行的函数
// 2.手写防抖效果
// 防抖核心:setTimeout实现
// 1.声明一个计时器变量
// 2.每次鼠标移动都先判断是否有定时器,如果有先清除以前的定时器
// 3.如果没有以前的定时器就开启定时器,存到变量里
// 4.定时器里实现函数调用
function debounce(fn,t){
let timer; // 声明计时器变量
// return 返回一个匿名函数 相当于将这个匿名函数给了下面的debounce
// 每触发一次事件 下面的debounce就会接收一个function
return function(){
if(timer) clearTimeout(timer); // 是否有定时器 清除
timer = setTimeout(function(){ // 开启定时器
fn(); //小括号调用
},t)
}
}
box.addEventListener('mousemove',debounce(mouseMove,500));
// 每次鼠标经过时就会 debounce - function()
5. 节流 throttle
节流:单位时间内,频繁触发事件,只执行一次
防抖是执行最后一个,节流是只能执行第一个
只有本次执行结束后才能执行下一次
使用场景:
高频事件:鼠标移动 mousemove、页面尺寸缩放 resize、滚动条滚动 scoll
仍然是上面的例子,鼠标在盒子上滑动,不管移动多少次,每隔500ms+1
5.1 lodash库
_.throttle(func, [wait = 0], [options = ])
在wait秒内最多执行func一次的函数
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
// 节流实现性能优化
// 需求:鼠标在盒子上移动 里面的数字就变化+1
// 首先获取盒子
const box = document.querySelector('.box')
let i = 1; // 设置初始
function mouseMove(){
box.innerHTML = i++;
// 如果存在大量消耗性能的代码 如DOM 数据处理等 可能造成卡顿
// 鼠标在盒子上停留500ms后 里面数字再加一
}
// 1.lodash
box.addEventListener('mousemove',_.throttle(mouseMove,500))
</script>
5.2 手写节流
节流核心:setTimeout实现
① 声明一个计时器变量
② 当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不再开始新定时器
③ 如果没有以前的定时器,就开启定时器,存到变量里
— 定时器里调用执行的函数
— 定时器里要把定时器清空 时间到了才开启下一个新的定时器
④ 定时器里调用要执行的函数
function throttle(fn,t){
let timer = null; //声明一个计时器变量
return function(){
if(!timer){
timer = setTimeout(function(){
fn();
// 清空定时器 才能开启下一个新的定时器
timer = null;
// 在定时器里面 不使用clearTimeout 在开启定时器的里面再关闭是不对的
},t)
}
}
}
box.addEventListener('mousemove',throttle(mouseMove,1000))
5.3 节流与防抖总结
本文介绍JS中的一些高阶技巧。