浅拷贝

用一个新的对象来接收 要复制的引用对象的值,如果对象的属性值是基本类型,复制将基本类型值给新对象,如果对象值是引用类型,复制的是引用类值的内存地址,存在共享,新对对象更改该引用类型内存地址的值,会影响另一个对象

只是创建了一个对象,拷贝了原有对象的基本数据类型的值,而引用类型只拷贝了一层的属性,再深层次就无法拷贝

使用 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 暂时无法满足的

  1. 拷贝对象的属性值是 函数、undefined、Symbol 这些类型,经过JSON.stringify 转换后键值会消失
  2. 拷贝Date引用类型会被转成字符串
  3. 无法拷贝不可枚举对象
  4. 无法拷贝对象的原型链
  5. 拷贝RegExp 引用类型会变成空对象
  6. 拷贝含有 NaN 和 Infinity,都会被处理成 null
  7. 无法拷贝对象的循环引用,即对象成环 (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

执行如下:

  1. 不能负责不可枚举的属性以及Symbol类型
  2. 这种只是对普通引用类型的值做递归复制,对于 RegExp, Error,Date 这样的引用类型,不能正确的拷贝
  3. 对象的属性里面成环,即循环引用没有解决

这种深拷贝是大家经常看到的,这种方式是有缺陷的

完善深拷贝

思路

针对不同问题采用不同解决方案

针对 不可枚举的属性和 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

  1. 能返回属性的特性的描述对象
    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 各个类型的能力
  • 考虑问题的全面性
    • 边界情况的考虑
    • 解决循环引用的能力