浅拷贝
用一个新的对象来接收 要复制的引用对象的值,如果对象的属性值是基本类型,复制将基本类型值给新对象,如果对象值是引用类型,复制的是引用类值的内存地址,存在共享,新对对象更改该引用类型内存地址的值,会影响另一个对象
只是创建了一个对象,拷贝了原有对象的基本数据类型的值,而引用类型只拷贝了一层的属性,再深层次就无法拷贝
使用 Object.assign()
通过object.assign 对象合并,按照从右往左的顺序进行合并,重名的属性,会被新的覆盖
注意:
不会拷贝对象的继承属性【prototype】
会拷贝对象的原型属性【 proto 】
可以拷贝symbol类型的属性
不会拷贝不可枚举的属性【不可枚举 确定是否会出现在for in 或者 Object.keys() 遍历中】
const obj = {
a: {
b:1
},
sym: Symbol(111)
}
function Name(){}
Name.prototype.haha2 = 22
const obj22 = new Name()
obj.__proto__.haha =11
Object.defineProperty(obj, 'hehe' ,{
value:'不可枚举属性',
enumerable:false
});
Object.defineProperty(obj, 'hehe2' ,{
value:'我是可枚举属性',
enumerable:true
});
const obj2 = {}
Object.assign(obj2, obj, obj22)
console.log(obj2)
obj2.a.b = 111
console.log("拷贝对象obj",obj) //{a: {b: 111}} hehe2:"我是可枚举属性" sym: Symbol(111) \ hehe: "不可枚举属性" )
// obj 不可枚举不会copy
console.log("原对象obj2",obj2) //a: {b: 111} 、 hehe2: "我是可枚举属性" 、 sym: Symbol(111)
console.log("拷贝原型",obj2.haha) // 拷贝原型 11
console.log("不可枚举",obj2.hehe) //不可枚举 undefined
console.log("可枚举属性",obj2.hehe2) // 可枚举属性 我是可枚举属性
console.log("prototype:",obj2.haha2) // prototype: undefined
使用解构
有着跟Object.assign 一样的缺陷,如果浅拷贝的都是基本类型,解构相比 Object.assign 更加方便一些
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
使用concat
concat 比较局限,只能用在数组的浅拷贝,concat拷贝一个包含引用类型的数组,修改原数组中的属性会影响连接后的数组
// 数组concat浅拷贝方法
let arr = [1, {a:1}, 3];
let newArr = arr.concat();
newArr[1].a = 100;
newArr[2] = 22;
console.log(arr); // [1, {a: 100}, 3]
console.log(newArr); // [1, {a:100}, 22]
使用 slice
slice 方法也是比较局限的,因为仅仅正对数组类型的,该方法通过前面两个参数来决定拷贝的开始位置和结束位置,不会改变原数组
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
以下是新的需要替换的的
手工实现一个浅拷贝
hasOwnProperty 检测属性是否微对象的自有属性,返回值是boolean,参数是检测的属性名;
不会对原型上的属性进行查找
function clone(target) {
if(typeof(target) == 'object' || target != null) {
const cloneTarget = Array.isArray(target) ? [] : {}
for(let prop in target) {
if(target.hasOwnProperty(prop)){
cloneTarget[prop] = target[prop]
}
}
return cloneTarget
} else {
return target
}
}
深拷贝
将一个对象完整的复制到另外一个新的对象,两者从内存上完全分离,互不影响,相互独立
将一个对象从内存中完整的拷贝给目标对象,并从堆内存中开辟新的空间存放新对象,且新对象的修改不会影响到原对象,二者实现真正分离
JSON.stringify
基本使用
目前开发中最简单的深拷贝的方法,就是把对象转成 json 字符串,再用JSON.parse 生成一个新的对象
//处理简单的数据深拷贝
let obj = {a:1,b:[2]}
let str = JSON.stringify(obj)
let obj2 = JSON.parse(str)
obj2.a = 'a'
obj2.b.push(3)
console.log(obj); //{a:1,b:[2]}
console.log(obj2); //{a:'a',b:[2,3]}
优点:
是最简单最快捷,满足日常开发需求
缺点:
对于比较麻烦的数据类型的属性,JSON.stringify 暂时无法满足的
- 拷贝对象的属性值是 函数、undefined、Symbol 这些类型,经过JSON.stringify 转换后键值会消失
- 拷贝Date引用类型会被转成字符串
- 无法拷贝不可枚举对象
- 无法拷贝对象的原型链
- 拷贝RegExp 引用类型会变成空对象
- 拷贝含有 NaN 和 Infinity,都会被处理成 null
- 无法拷贝对象的循环引用,即对象成环 (obj[key = obj])
let obj = {
a:1,
b:[2],
fun:function(){},
unde:undefined,
sym:Symbol(1),
date:new Date(),
reg: /1/,
nan: NaN,
infinity: Infinity
}
Object.defineProperty(obj, 'sex', {
eumerable: false,
value:'男'
})
let str = JSON.stringify(obj)
let obj2 = JSON.parse(str)
obj2.a = 'a'
obj2.b.push(3)
console.log(obj)
console.log(obj2)
执行结果如下:
手写基础版 递归深拷贝
let obj = {
a:1,
b:[2],
fun:function(){console.log(111)},
unde:undefined,
date:new Date(1),
err: new Error('111'),
reg: /1/,
nan: NaN,
[Symbol('1')]:1,
infinity: Infinity
}
function deepClone(obj) {
if(typeof(obj) === 'object' || obj != null) {
const cloneData = Array.isArray(obj) ? [] : {}
for(let key in obj) {
if(typeof(obj[key]) === 'object'){
cloneData[key] = deepClone(obj[key])
} else {
cloneData[key] = obj[key]
}
}
return cloneData
} else {
return obj
}
}
const obj2 = deepClone(obj)
obj2.b.push(3)
console.log(obj)
console.log(obj2)
obj2.fun() // 111
执行如下:
- 不能负责不可枚举的属性以及Symbol类型
- 这种只是对普通引用类型的值做递归复制,对于 RegExp, Error,Date 这样的引用类型,不能正确的拷贝
- 对象的属性里面成环,即循环引用没有解决
这种深拷贝是大家经常看到的,这种方式是有缺陷的
完善深拷贝
思路
针对不同问题采用不同解决方案
针对 不可枚举的属性和 Symbol,使用 Reflect.ownKeys 方法,可以遍历对象的不可枚举属性和Symbol。
当属性值为 Date 和 RegExp 类型的时候,通过 new 生成示例
利用 Object .getOwnPropertyDescriptors 获取对象的所有属性,以及对应的特性,对应的值就是描述对象;利用Object.getPrototypeOf 返回指定对象的原型,顺便使用Object.create 创建一个新的对象,并继承传入源对象的原型链
说白了就是对各个类型做兼容
代码
// 处理只有引用类型
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj != null)
const deepClone = function (obj, hash = new WeakMap()) {
if(obj.constructor === Date) return new Date(obj) // 日期对象返回一个新的日期对象
if(obj.constructor === RegExp) return new RegExp(obj) // 正则对象返回一个新的正则对象
if(hash.has(obj)) return hash.get(obj) //如果检测到了循环引用,就使用 WeakMap 来解决
let allDesc = Object.getOwnPropertyDescriptors(obj) // 获取对象属性特性的描述对象用于,主要用于克隆
//getPrototypeOf 解决无法拷贝原型
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
hash.set(obj, cloneObj)
for(let key of Object.keys(obj)){
// console.log(key)
// 必须是对象 或者数组,这段代码再其他深拷贝经常看到
cloneObj[key] = (isComplexDataType(obj[key])) && typeof obj[key] !== 'function' ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 验证
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ca7Lnm4s-1652937625959)(C:\Users\lx\AppData\Roaming\Typora\typora-user-images\image-20220519131150481.png)]
知识点
Reflect.ownKeys
返回对象的所有属性,包括不可枚举的属性和 Symbol
Object.keys 只返回可枚举的属性,不处理Symbol
let obj = {
a: 1,
[Symbol(1)]: 2
}
Object.defineProperty(obj, 'sex', {
enumerable: false,
value:2
})
console.log(Reflect.ownKeys(obj)) // ['a', 'sex', Symbol(1)]
console.log(Object.keys(obj)) //['a']
console.log(Reflect.ownKeys([1]))
Object.getOwnPropertyDescriptors
- 能返回属性的特性的描述对象
2.可以读取到 set属性和 get 属性的对象
实际开发作用:克隆对象
var obj = {
a: 1,
set fun(val) {
this.a = val
},
get bar(){return "bar返回字符串"}
}
console.log(obj)
console.log(Object.getOwnPropertyDescriptors(obj))
Object.getPrototypeOf
获取返回指定对象的原型,是获取原型对象的标准方法
可以用来从字类上获取父类,因此可以用于判断,一个类是否继承了另外一个类。
function Bar(){}
const obj = new Bar()
console.log(Object.getPrototypeOf(obj) === Bar.prototype)
Reflect.ownKeys
返回一个对象的属性键组成的数组
const obj = {
a: 1,
b:2
}
const arr = [1]
Reflect.ownKeys(obj) // [a,b]
Reflect.ownKeys(arr) //[0, length]
WeakMap
这个知识点比较复杂,涉及到垃圾回收机制;垃圾回收机制又有 引用计数和标记清除的方式
描述
WeakMap 是对象的键/值对的集合,其中的键是弱引用的,必须是对象,值可以是任意的
WeakMap 的key只能是Object 类型,原始数据类型不能作为key,比如Symbol。
如果一个变量保存着一个对象的强引用,那这个对象不会被垃圾回收,如果一个变量保存着对象的弱引用,那么这个对象会被垃圾回收。
WeakMap 的成员随时可能被垃圾回收机制回收
WeakMap 能干什么
可以使用WeakMap 防止内存泄漏的场景:
保留关于特定对象的私有数据,并且只将该对象的访问权限授予Map的引用者
报错有关对象的数据而不更改它们或产生开销。
在浏览器中保存有关宿主对象( DOM 节点) 的数据
从外部向对象添加功能
WeakMap 实例方法
WeakMap.prototype.delete(key)
删除WeakMap key 相关联的值,删除后, WeakMap.prototype.has(key) 会返回false
WeakMap.prototype.get(key)
返回一个WeakMap key相关联的的值,如果key不存在,返回 undefined
WeakMap.prototype.has(key)
返回一个布尔值,判断一个值 是否与 WeakMap 对象的 key 有关联
WeakMap.prototype.set(key, value)
给 WeakMap 设置一个 value 值,返回一个 WeakMap 对象
什么是弱引用
普通对象在其他地方不再引用该对象,那么垃圾回收机制会自动回收该对象占用的内存。
浏览器的 垃圾回收机制 不会考虑该对象的引用
WeakMap 与 Map 的区别
使用 map,对象会占用内存,可能不会被垃圾回收。Map对一个对象是强引用。
WeakMap 不会阻止关键对象的垃圾回收。
WeakMap 不能进行遍历, Map 支持遍历
WeakMap 没有 clear(),Map 中有定义该方法
WeakMap 只支持对象作为key,其他类型作为key报错,Map 没有这限制
垃圾回收机制
我都知道,程序运行中有些垃圾数据不再使用,需要及时释放出去,如果没有及时释放,也就是内存泄漏。
js 中的垃圾数据都是由垃圾回收(GC)器自动回收的,不需要手动释放
查找内存中的垃圾,回收空间和释放空间
什么是垃圾数据
- 程序中不需要再使用的对象
- 程序中不能再访问的对象
引用计数
核心:计数器,维护对象的引用数,判断对象的引用数是否为0,引用数为0,立即回收
维护全局对象的引用数
理解就是:对象有没有被其他地方用到,全局能否访问
const obj1 = {name:1} //不会被回收,因为被下面引用了,引用数不为0
const arrList = [obj1.name];
function fun() {
name = 'start'; // name变量在函数执行完后,全局还能方法,此刻引用数就是1,就不会被回收
const age = 123; // age 只能在当前函数访问,执行完,全局无法访问,全局无法再指向它了,引用数是0,立即回收
}
fun()
优点:
可以即时回收垃圾对象,发现垃圾时能即时回收,因为是时刻检测引用。
减少程序的卡顿时间,最大程度的减少程序的暂缓,因为时刻在检测引用数立即回收,减少了程序的占满。
缺点:
无法回收循环引用的对象
时间开销大,频繁操作会导致资源销毁过大
标记清除
核心:将整个垃圾回收分为两个阶段:标记阶段和清除阶段
- 遍历所有对象标记可达对象(全局能访问的对象),不可达的对象不标记;
- 遍历清除所有未标记的对象;回收空间,释放在空闲链表上;
- 再清除标记;
优点:
解决了引用计数算法无法回收循环引用对象的缺点
缺点:
回收的空间比较散乱;浪费空间,产生空间碎片化问题。
空间化碎片:回收的垃圾对象,放在空闲链表上是没有顺序的,不连续的,当需要指定字节大小空间的时候,回收的大小刚好又不匹配,占用了内存空间照成了内存浪费。
为解决这一空间碎片化问题:解决方法是标记整理算法。
标记整理算法
使用标记算法 实现整理标记回收空间,标记清除增强版
标记阶段是一样的,再清除前会执行整理,移动对象位置。
优点:
减少碎片化空间
缺点:
不会立即回收垃圾对象
总结
考察的能力
- 基础编码能力
- 递归编码能力
- 代码的严谨性
- 代码的抽象能力
- JS 编码能力
- 熟练掌握Weak Map 的能力
- 引用类型各种API的熟练程度
- 准确判断 JS 各个类型的能力
- 考虑问题的全面性
- 边界情况的考虑
- 解决循环引用的能力