浅拷贝与深拷贝
浅拷贝:就是将定义的对象指向目的对象的地址,所以当我们对新创建的对象进行修改时,原来的目标对象也被修改了。(下文中,将只执行了第一层深拷贝化的做法,即没有完全深拷贝的做法也称为浅拷贝了。)
深拷贝:就是让新创建的对象有一个全新的地址,然后内容和原来的对象一样。
如果对象内的属性或者数组的元素都是值类型,那么我们无需考虑这一问题(只要实现一层深拷贝就完成了,很简单),但是当其是引用类型的时候,就需要实现深拷贝来实现数据的隔离了。
对于对象,可以使用扩展运算符和Object.assign来实现浅拷贝
let a = {
name:'qqq',
age:18,
sign:'manba'
}
let b = {...a}
b['name'] = 'bbb';
console.log(a);
console.log(b);
let a = {
name:'qqq',
age:18,
sign:'manba'
}
let b = {}
Object.assign(b,a);
b['name'] = 'bbb';
console.log(a);
console.log(b);
对于数组也可以使用扩展运算符,还可以使用concat和slice来实现浅拷贝
slice() 方法可从已有的数组中返回由选定的元素组成的数组。两个参数(start,end),都是可选的,如果不填就是整个复制了。
let arr1 = [1,2,3,4,5]
let arr2 = [...arr1]
let arr3 = arr1.slice()
let arr4 = [].concat(arr1)
console.log(arr1,arr2,arr3,arr4)
对于深拷贝应该思考一个问题,之前就反复在说,引用类型中的栈放的是地址,那么如果对象中有引用类型的属性,一次的深拷贝肯定是不够的。需要对引用类型的属性也进行深拷贝,如此循环直至结束。
深拷贝的方法(1、使用JSON 2、手写循环深拷贝)
1、JSON来实现深拷贝
基于JSON.stringify将对象先转成字符串,再通过JSON.parse将字符串转成对象,此时对象中每个层级的堆内存都是新开辟的。
let obj = {
name:'CK',
age:22,
skills:{
run:'run',
play:'play'
}
}
let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj,obj2)
console.log(obj.skills === obj2.skills) // false
2、手写
思路:判断类型+拷贝+循环
首先判断当前的类型,如果是引用类型,那么遍历属性并对每个属性进行递归操作,如果是值类型,那么不需要进行递归,直接返回即可。
判断类型的方法
- typeof()
只能判断基本类型,不能分辨Object和Array - toString()
通过这段代码可以获取一个变量的类型(非常好用)
Object.prototype.toString.call(obj).slice(8,-1)
- Array.isArray()
可以搭配typeof()来实现完整的类型判断
function checkType (target) {
let result = Object.prototype.toString.call(target).slice(8,-1);
return result === 'Array' ? 'Array' : result === 'Object' ? 'Object' : 'Normal'
}
function deepClone (target) {
if(checkType(target) === 'Normal') return target
let result = checkType(target) === 'Array' ? []:{} // 要用一个新的地址
Object.keys(target).forEach(item => {
result[item] = deepClone(target[item])
})
return result
}
let obj = {
name:'CK',
age:22,
skills:{
run:'run',
play:'play'
}
}
let obj2 = deepClone(obj)
obj2.skills.run = 'quickly run'
console.log(obj,obj2)
联系原型的思考
一直在思考,原型链的继承是深拷贝还是浅拷贝呢?
先说结论:也是一种浅拷贝,或者说只进行了第一层深拷贝。
首先复习一下原型链的一些知识点。
- 注意区分显式原型prototype和隐式原型[[prototype]],后者是一个内部属性一般看不到。 可以通过__proto__属性在Chrome浏览器中使用。
- 对象的__proto__引用值指向创建这个对象的构造函数的prototype对象
- 所有prototype对象都是由Object创建的,除了Object.prototype对象本身
- 所有构造函数本身都是由Function创建的,除了Function本身
对于原型链的取值很好理解,当前实例中没有时会沿着原型链一直往上寻找。
但是赋值就比较复杂了,是会一直往上寻找值然后修改吗?
之前的学习中了解到,在使用new的时候,会把新创建实例的[[prototype]](也就是__proto__)和构造函数的prototype进行一个赋值。那么这里就要思考一个问题,如果是浅拷贝的话,那不是会出现一些问题吗?各个实例之间的内容就会互相影响了。
这里必然是有些奇妙之处的。
根据代码可见,对于值类型的属性a中对于原型链上属性的更改并不会影响到b。
这是因为:此时会在原对象上对该属性进行赋值修改操作,而不是在原型链上的这个对象上修改。(如果该属性存在Setter的话也可能对原型链上的值进行修改)
那么可以联想到,如果是引用类型的话,这个赋值就会导致各个实例互相影响的情况
值类型
class Human {
constructor(){}
}
Human.prototype.we = 'human'
let a = new Human()
let b = new Human()
a.we = 'a'
console.log(a.we) // a
console.log(b.we) // human
引用类型
class Human {
constructor(){}
}
Human.prototype.we = {name:'human'}
let a = new Human()
let b = new Human()
a.we.name = 'a'
console.log(a.we) // { name: 'a' }
console.log(b.we) // { name: 'a' }
关于函数继承的思考
正因为通过原型链来获取属性会导致实例互相影响的情况,所以我们在继承中对于属性使用this调用父类的构造函数。对于方法使用原型链(但是在一个实例的原型中添加方法时其他实例以及原先的构造函数也可以通过实例使用方法)。
Father.call(this, 参数);
静态属性/方法和原型链上的属性/方法
明确原型链是针对实例的就好了。