JavaScript 进阶 - 第 4 天
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
,因为简单类型直接拷贝值了
浅拷贝:拷贝的是地址,只拷贝一层
直接赋值的情况:
浅拷贝的情况:
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
- 拷贝数组:Array.prototype.concat() 或者 […arr]
如果是简单数据类型拷贝值,如果是引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题),也就是只拷贝第一层
里面的属性值是简单数据类型就直接拷贝值;
如果属性值是引用数据类型则拷贝的是地址;
补充Object.assign的使用
如果只有一个参数,Object.assign会直接返回该参数。
要使用 Object.assign()
方法来实现浅拷贝,你需要确保将目标对象作为第一个参数传递给 Object.assign()
,然后将源对象作为后续参数传递。这样 Object.assign()
将会将源对象的所有可枚举属性复制到目标对象中,从而实现浅拷贝。
下面是一个示例:
const obj1 = { name: "Alice" };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { name: "Alice" }
console.log(obj1 === obj2); // false,因为它们是两个不同的对象
在这个例子中,我们创建了一个空对象 {}
作为目标对象,然后将 obj1
的属性复制到这个目标对象中,最终得到了 obj2
。这样就实现了对 obj1
的浅拷贝。
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- js 的库:lodash/cloneDeep
- 通过 JSON.stringify()实现
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
<body>
<script>
const obj = {
uname: "pink",
age: 18,
hobby: ["乒乓球", "足球"],
family: {
baby: "小pink",
},
};
const o = {};
// 拷贝函数
function deepCopy(newObj, oldObj) {
debugger;
for (let k in oldObj) {
// k 是属性名,oldObj[k]是属性值
// 处理数组的问题 一定先写数组 在写 对象 不能颠倒,因为一切皆对象,要是把对象写前面,Array就永远没有机会了
if (oldObj[k] instanceof Array) {
newObj[k] = [];
// newObj[k] 接收 [] hobby
// oldObj[k] ['乒乓球', '足球']
deepCopy(newObj[k], oldObj[k]);
} else if (oldObj[k] instanceof Object) {
newObj[k] = {};
deepCopy(newObj[k], oldObj[k]);
} else {
// 只有一层的简单数据
// k 属性名 uname age oldObj[k] 属性值 18
// newObj[k] === o.uname 给新对象添加属性
newObj[k] = oldObj[k];
}
}
}
deepCopy(o, obj); // 函数调用 两个参数 o 新对象 obj 旧对象
console.log(o);
o.age = 20;
o.hobby[0] = "篮球";
o.family.baby = "老pink";
console.log(obj);
console.log([1, 23] instanceof Object);
// 复习
// const obj = {
// uname: 'pink',
// age: 18,
// hobby: ['乒乓球', '足球']
// }
// function deepCopy({ }, oldObj) {
// // k 属性名 oldObj[k] 属性值
// for (let k in oldObj) {
// // 处理数组的问题 k 变量
// newObj[k] = oldObj[k]
// // o.uname = 'pink'
// // newObj.k = 'pink'
// }
// }
</script>
</body>
const arr = [1, 2];
// 处理数组的问题 一定先写数组 在写 对象 不能颠倒,因为一切皆对象,要是把对象写前面,Array就永远没有机会了
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
js 库 lodash 里面 cloneDeep 内部实现了深拷贝
<body>
<!-- 先引用 -->
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: "pink",
age: 18,
hobby: ["乒乓球", "足球"],
family: {
baby: "小pink",
},
};
const o = _.cloneDeep(obj); // 写法规范就是下划线 _.
console.log(o);
o.family.baby = "老pink";
console.log(obj);
</script>
</body>
JSON 序列化
JSON.stringify(obj)
把对象转换成JSON字符串
JSON.parse(obj)
把字符串转换成对象
<body>
<script>
const obj = {
uname: "pink",
age: 18,
hobby: ["乒乓球", "足球"],
family: {
baby: "小pink",
},
};
// 把对象转换为 JSON 字符串
// console.log(JSON.stringify(obj))
const o = JSON.parse(JSON.stringify(obj));
console.log(o);
o.family.baby = "123";
console.log(obj);
</script>
</body>
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
throw
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
<script>
function counter(x, y) {
if (!x || !y) {
// throw '参数不能为空!';
throw new Error("参数不能为空!");
}
return x + y;
}
counter();
</script>
总结:
throw
抛出异常信息,程序也会终止执行throw
后面跟的是错误提示信息Error
对象配合throw
使用,能够设置更详细的错误信息
try … catch
<script>
function foo() {
try {
// 查找 DOM 节点
const p = document.querySelector(".p");
p.style.color = "red";
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息
console.log(error.message);
// 终止代码继续执行
return;
} finally {
alert("执行");
}
console.log("如果出现错误,我的语句不会执行");
}
foo();
</script>
总结:
try...catch
用于捕获错误信息- 将预估可能发生错误的代码写在
try
代码段中 - 如果
try
代码段中出现错误后,会执行catch
代码段,并截获到错误信息
debugger
相当于断点调试
处理 this
了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。
this
是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this
的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this
默认的取值】情况进行归纳和总结。
普通函数
普通函数的调用方式决定了 this
的值,即【谁调用 this
的值指向谁】,如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this);
}
// 函数表达式
const sayHello = function () {
console.log(this);
};
// 函数的调用方式决定了 this 的值
sayHi(); // window
window.sayHi();
// 普通对象
const user = {
name: "小明",
walk: function () {
console.log(this);
},
};
// 动态为 user 添加方法
user.sayHi = sayHi;
uesr.sayHello = sayHello;
// 函数调用方式,决定了 this 的值
user.sayHi(); // user
user.sayHello(); // user
</script>
注: 普通函数没有明确调用者时 this
值为 window
,严格模式下没有调用者时 this
的值为 undefined
。
箭头函数
箭头函数中的 this
与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this
。 箭头函数中访问的 this
不过是箭头函数所在作用域的 this
变量。
<script>
console.log(this); // 此处为 window
// 箭头函数
const sayHi = function () {
console.log(this); // 该箭头函数中的 this 为函数声明环境中 this 一致
};
// 普通对象
const user = {
name: "小明",
// 该箭头函数中的 this 为函数声明环境中 this 一致
walk: () => {
console.log(this); // 箭头函数本身没有this,所以往上找,user是对象,也没有this,所以继续向上找,所以最后是window
},
sleep: function () {
let str = "hello";
console.log(this);
let fn = () => {
console.log(str);
console.log(this); // 该箭头函数中的 this 与 sleep 中的 this 一致
};
// 调用箭头函数
fn();
},
};
// 动态添加方法
user.sayHi = sayHi;
// 函数调用
user.sayHi();
user.sleep();
user.walk();
</script>
在开发中【使用箭头函数前需要考虑函数中 this
的值】,事件回调函数使用箭头函数时,this
为全局的 window
,因此 DOM 事件回调函数不推荐使用箭头函数,如下代码所示:
<body>
<button id="pu">click普通函数</button>
<button id="jian">click箭头函数</button>
</body>
<script>
document.querySelector("#pu").addEventListener("click", function () {
console.log("click普通函数", this); // button
});
document.querySelector("#jian").addEventListener("click", () => {
console.log("箭头函数", this); // window
});
</script>
<script>
// DOM 节点
const btn = document.querySelector(".btn");
// 箭头函数 此时 this 指向了 window
btn.addEventListener("click", () => {
console.log(this);
});
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener("click", function () {
console.log(this);
});
</script>
同样由于箭头函数 this
的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
构造函数和原型的this都指向实例对象
<script>
function Person() {}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log("人都要走路...");
console.log(this); // window
};
const p1 = new Person();
p1.walk();
</script>
改变 this 指向
以上归纳了普通函数和箭头函数中关于 this
默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this
的指向,有 3 个方法可以动态指定普通函数中 this
的指向:
call (了解即可
作用:调用函数 + 改变this指向
使用 call
方法调用函数,同时指定函数中 this
的值,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: "小明",
age: 18,
};
let student = {
name: "小红",
age: 16,
};
// 调用函数并指定 this 的值
sayHi.call(user); // this 值为 user
sayHi.call(student); // this 值为 student
// 求和函数
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result);
</script>
总结:
call
方法能够在调用函数的同时指定this
的值- 使用
call
方法调用函数时,第 1 个参数为this
指定的值 call
方法的其余参数会依次自动传入函数做为函数的参数- 返回值就是函数的返回值
apply
使用 apply
方法调用函数,同时指定函数中 this
的值,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: "小明",
age: 18,
};
let student = {
name: "小红",
age: 16,
};
// 调用函数并指定 this 的值
sayHi.apply(user); // this 值为 user
sayHi.apply(student); // this 值为 student
// 求和函数
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10]);
console.log(result);
</script>
总结:
apply
方法能够在调用函数的同时指定this
的值- 使用
apply
方法调用函数时,第 1 个参数为this
指定的值 apply
方法第 2 个参数为数组,数组的单元值依次自动传入函数做为函数的参数- 返回值就是函数的返回值
bind ( 重点
bind
方法并不会调用函数,而是创建一个指定了 this
值的新函数,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: "小明",
age: 18,
};
// 调用 bind 指定 this 的值
let sayHello = sayHi.bind(user);
// 调用使用 bind 创建的新函数
sayHello();
</script>
注:bind
方法创建新的函数,与原函数的唯一的变化是改变了 this
的值。
小结
三者区别:
- 参数形式:call 和bind 是直接穿参数 ,apply是数组的形式传递参数
- 是否会调用函数:call 和apply 会调用;但是bind不会调用
- 返回值:bind返回由指定的this和初始化参数改造的
新函数
(原函数拷贝); 而call和bind的返回值是函数的返回值
防抖节流
防抖(debounce)
概念
单位时间内,频繁出发事件,只执行一次,且只执行最后一次。
只要被打断就重新来。
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
使用场景:
- 搜索框搜索输入,只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入
代码实战
节流(throttle)
概念
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数,(只执行第一次
使用场景
代码实战
总结
【节流】综合案例——跳转到历史播放时刻
onloaddata也可以理解成 打开页面触发事件
但是ontimeupdate的出发频次太高了,需要节流,可以设置1秒钟触发一次