1 深浅拷贝
拷贝,一个大家都非常熟悉的操作,对于每天都会使用计算机的人来说可能已经用了上万次甚至几十万次的拷贝操作了,但这里说的深拷贝和浅拷贝与我们日常接触到的拷贝操作有点不一样,这两种拷贝只针对引用类型的数据而非文件。
1.1 浅拷贝
我们都知道,对于引用类型来说,变(常)量的名称对应的是一个指针,这个指针指向了数据所存放的空间,而浅拷贝就是将这个指针原封不动的给到另一个变(常)量名。常见的方法有:
① Object.assgin() / 展开运算符 {...obj} (拷贝对象)
② Array.prototype.concat() 或者 [...arr](拷贝数组)
<script>
const ob = {
uname: 'aabb',
age: 18,
}
const a = {...ob}
const b = {}
const c = ob //直接赋值
Object.assign(b,ob)
console.log(a);
console.log(b);
console.log(c); //直接赋值
</script>
在上面的代码中除了浅拷贝的两种方法外还有一种直接赋值的方法,直接赋值的方法与浅拷贝的区别在于,直接赋值是将指向同一个空间的指针赋给了两个不同的变(常)量,只要修改其中一个变(常)量,则另一个变(常)量的属性值也会改变,
<script>
const ob = {
uname: 'aabb',
age: 18,
}
const c = ob
Object.assign(b,ob)
console.log(c);
c.uname="abc"
console.log(ob);
console.log(c);
</script>
如果是浅拷贝,则两个变(常)量的属性值不会相互影响,
<script>
const ob = {
uname: 'aabb',
age: 18,
}
const a = {...ob}
const b = {}
Object.assign(b,ob)
console.log(a);
console.log(b);
a.uname = "aaccqq"
console.log(a);
console.log(b);
</script>
但是,如果原本对象里还有引用类型的数据,情况就不一样了,
<script>
const ob = {
uname: 'aabb',
age: 18,
home:{
province:'xxx',
city:'xxx',
}
}
const a = {...ob}
const b = {}
Object.assign(b,ob)
a.home['province']='qqq'
console.log(a);
console.log(b);
</script>
明明只改动了 a 中 home 属性中的 province 属性,但 b 中的 province 却也改变了,出现这种现象的原因就在于浅拷贝时是将内部的属性和方法通过直接赋值的形式拷贝到了另一个变(常)量里去,若是遇到了引用类型的属性或方法,那么它拷贝过去的仅仅是这种属性或方法的地址,若是想要解决这个问题,就需要用到深拷贝方法。
1.2 深拷贝
为了解决浅拷贝的问题,又出现了各种深拷贝的方法,常见的方法有:
① 通过递归实现深拷贝
<script>
const obj = {
uname: 'aaabbb',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: 'cccddd'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
debugger
for (let k in oldObj) {
// 处理数组的问题 一定先写数组再写对象 不能颠倒
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)
console.log(o)
</script>
② lodash/cloneDeep
利用 lodash 库中的 _.cloneDeep(obj) 函数来实现深拷贝,
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: 'aabb',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: 'ccdd'
}
}
const o = _.cloneDeep(obj)
console.log(o)
o.family.baby = 'aabb'
console.log(obj)
</script>
③ 通过JSON.stringify()实现
可以先将引用类型数据转换为 JSON 字符串,然后将 JSON 字符串通过直接赋值的方式拷贝给另一个变(常)量,然后再通过解析的方式将 JSON 字符串转换成对象。
<script>
const obj = {
uname: 'aabb',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: 'ccdd'
}
}
const o = JSON.parse(JSON.stringify(obj)) //将转换成JSON字符串的数据解析回去后直接赋值给新的常量o
console.log(o)
o.family.baby = '123'
console.log(obj)
</script>
2 异常处理
2.1 throw 抛出异常
throw 的作用是抛出异常信息并且终止程序。抛出的异常信息为 throw 后紧接着的内容,一般使用 Error 对象进行描述,使其更加具体。
<script>
function fn(x, y) {
if (!x || !y) {
// throw '没有参数传递进来'
throw new Error('没有参数传递过来')
}
return x + y
}
console.log(fn())
console.log('123');
</script>
![](https://i-blog.csdnimg.cn/blog_migrate/ff33015ffa7168dd05fb28dc571a07aa.png)
2.2 try/catch 捕获异常
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) {
// 拦截错误,提示浏览器提供的错误信息,但是不中断程序的执行
console.log(err.message)
}
finally {
// 不管你程序对不对,一定会执行的代码
console.log('不管你程序对不对,一定会执行的代码');
}
console.log(11)
}
fn()
console.log('123');
</script>
</body>
与 throw 抛出异常不同的是,try / catch 组合能够让代码在出错的情况下继续执行下去,
在 try 中写上可能会出错的代码,一旦出错就会被 catch 捕获并执行 catch 内代码,finally 大括号内的代码则是不管是否出错都会执行,并且在 finally 中的代码执行完毕后还能接下往下执行,直到整个文件里的代码全部执行完毕。
3 this 指向问题
3.1 普通函数
普通函数的 this 指向调用它的对象,
<body>
<button>点击</button>
<script>
// 普通函数: 谁调用我,this就指向谁
console.log(this) // window
function fn() {
console.log(this) // window
}
window.fn()
window.setTimeout(function () {
console.log(this) // window
}, 1000)
document.querySelector('button').addEventListener('click', function () {
console.log(this) // 指向 button
})
const obj = {
sayHi: function () {
console.log(this) // 指向 obj
}
}
obj.sayHi()
</script>
</body>
3.2 箭头函数
箭头函数与普通函数不同,它会默认绑定外层 this 的值,因此在箭头函数中 this 的值和外层的 this 是一样的,在寻找外层 this 的过程中,它会沿着作用域一层层地向外查找,直至找到具有意义的 this 为止。
<script>
const ob = {
that:this,
a:12,
func:function(){
console.log(this);
},
func2:()=>{
console.log(this);
}
}
console.log(ob.that);
ob.func()
ob.func2()
</script>
3.3 改变 this 指向
在 JavaScript 中,可以通过 call() 、 apply() 、bind() 这三种方法指定函数中 this 的指向。
1.call()
call() 的基本语法为
其中,thisArg 为 this 需要指向的对象, arg1、arg2 为传入函数的参数
<script>
const obj = {
uname: 'pink'
}
function fn(x, y) {
console.log(this) // window
console.log(x + y)
}
// 1. 调用函数
// 2. 改变 this 指向
fn.call(obj, 1, 2)
const newFunc = (x,y) => {
console.log(this);
console.log(x+y);
}
newFunc.call(obj,1,2)
const newFunc1 = function(x,y){
console.log(this);
console.log(x+y);
}
newFunc1.call(obj,1,2)
</script>
call() 只对普通函数有效,对箭头函数无效。
2.apply()
apply() 的基本语法为
其中,thisArg 为 this 需要指向的对象,argsArray 为数组,内含所传递的值,
<script>
const obj = {
age: 18
}
const newFunc = (x,y) => {
console.log(this);
console.log(x+y);
}
newFunc.apply(obj,[1,2])
const newFunc1 = function(x,y){
console.log(this);
console.log(x+y);
}
newFunc1.apply(obj,[1,2])
</script>
apply() 只对普通函数有效,对箭头函数无效。
call() 和 apply() 都是更改 this 指向的函数,也同样是调用函数的函数,它们的区别体现在传入的参数的形式不一样,call() 为单独的参数,而 apply() 必须传入一个数组。
3.bind()
bind() 方法与前两者不同的是它不会调用函数,而仅是改变函数内部 this 的指向,并返回一个新的函数,其语法为
其中,thisArg 为 this 需要指向的对象, arg1、arg2 为传入函数的参数
<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)
fn()
fun()
</script>
4. call() apply() bind() 的异同
(1)相同点
都可以改变函数内部 this 的指向
(2)不同点
① call() 和 apply() 在改变函数内部 this 指向的同时还会调用函数,但 bind() 并不会调用函数,而是仅在返回的函数中改变 this 指向。
② call() / bind() 和 apply() 传递的参数不一样, call() / bind() 传递参数为 arg1, arg2... 的形式, apply() 参数的形式必须为数组 [arg]。