关于前端:写个JS深拷贝面试备用

7次阅读

共计 9514 个字符,预计需要花费 24 分钟才能阅读完成。

深拷贝浅拷贝和赋值的原理及实现分析

在工作中咱们常常会用到深拷贝与浅拷贝,然而你有没有去剖析什么场景下应用它,为什么须要应用呢,深浅拷贝有何异同呢,什么是深拷贝呢,如何实现呢,你会有这些问题吗,明天就为大家总结一下吧。

栈内存与堆内存
区别
  • 浅拷贝 — 拷贝的是一个对象的指针, 而不是复制对象自身,拷贝进去的对象共用一个指针,其中一个扭转了值,其余的也会同时扭转。
  • 深拷贝 — 拷贝进去一个新的对象,开拓一块新的空间,拷贝前后的对象互相独立,相互不会扭转,领有不同的指针。

简略的总结下,假如有个 A,咱们拷贝了一个为 B,就是批改 A 或者 B 的时候看看另一个会不会也变动,如果扭转 A 的值 B 也变了那么就是浅拷贝,如果扭转 A 之后 B 的值没有发生变化就是深拷贝,当然这是根底了解,上面咱们一起来剖析下吧。

赋值
/** demo1 根本数据类型 */
let a = 1;
let b = a;
b = 10;
console.log(a,b)//  1    10
/** demo2 援用数据类型 */
let a = {
    name: '小九',
    age: 23,
    favorite: ['吃饭','睡觉','打豆豆']
}
let b = a;
a.name = '小七'
a.age = 18
a.favorite = ['下班','上班','加班']
console.log(a,b)
/** {name: '小七', age: 18, favorite: [ '下班', '上班', '加班'] } {name: '小七', age: 18, favorite: [ '下班', '上班', '加班'] }*/

通过看下面的例子能够看出通过赋值去拿到新的值,赋值对于根本数据来说就是在栈中新开了一个变量,相当于是两个独立的栈内存,所以互相不会影响,然而对于援用数据类型,他只是复制了一份 a 在栈内存的指针,所以两个指针指向了同一个堆内存的空间,通过任何一个指针扭转值都会影响其余的,通过这样的赋值能够产生多个指针,然而堆内存的空间始终只有一个,这就是赋值产生的问题,咱们在开发中当然不心愿扭转 B 而影响了 A,所以这个时候就须要用到浅拷贝和深拷贝了。

  • 针对根本数据类型,轻易赋值都不会相互影响
  • 针对援用数据类型,赋值就会呈现咱们不想看到的,改变一方单方都变动。
浅拷贝
Object.assign()
/** Object.assign */
let A = {
    name: '小九',
    age: 23,
    sex: '男'
}
let B = Object.assign({}, A);
B.name = '小七'
B.sex = '女'
B.age = 18
console.log(A,B)
/** {name: '小九', age: 23, sex: '男'} {name: '小七', age: 18, sex: '女'} */

首先实现浅拷贝的第一个办法是通过 Object.assign()这个 办法,Object.assign() 办法用于将所有可枚举属性的值从一个或多个源对象复制到指标对象。它将返回指标对象。

先不论这个办法具体干嘛,咱们先来看看后果,咱们发现拷贝一个 A 之后的 B 扭转了 nameagesex 之后 A 的值并没有发生变化,在这里,你可能会想,这不是就胜利了么,AB 宜家相互不影响了,可是和咱们下面讲的浅拷贝会 AB 相互不变动就是深拷贝产生了矛盾,那么是为什么呢,其实下面曾经说到了,这个 demo 外面用到的全是根本数据类型,所以拷贝和赋值一样,针对根本数据类型,都是在栈从新开拓一个变量,所以互相不会影响,那咱们看看援用数据类型,

let A = {
    name: '小九',
    age: 23,
    sex: '男',
    favorite: {item_a:['打游戏','上网'],
        item_b:['读书','网课']
    }
}
let B = Object.assign({}, A);

B.name = '小七'
B.sex = '女'
B.age = 18
B.favorite.item_a =['打篮球'] 
B.favorite.item_b =['写笔记'] 
console.log(A)
console.log(B)
/** 打印后果比照 */
{ name: '小九',
  age: 23,
  sex: '男',
  favorite: {item_a: [ '打篮球'], item_b: ['写笔记'] } }
----------------------------------------------------------------
{ name: '小七',
  age: 18,
  sex: '女',
  favorite: {item_a: [ '打篮球'], item_b: ['写笔记'] } }

通过比照发现咱们同样拷贝了 A 之后发现扭转 B 的 name age sex 都不会影响,然而扭转 facorite 的时候却影响了 A,那么问题来了,这咱们通过浅拷贝发现仍然无奈满足咱们的需要,扭转 B 同样影响了 A,回到这个办法,Object.assign()这个办法是能够把任意的多个源对象的可枚举属性拷贝给指标对象,而后返回指标对象,它进行的是 对象的浅拷贝,拷贝的是对象的援用,而不是对象自身 ,所以针对于这种有两层的数据结构就出呈现只拷贝了第一层,第二层以下的对象仍然拷贝不了,所以咱们称Object.assign() 为浅拷贝,只有在对象只有一层构造的时候才时候应用,

  • 很多人说 Object.assign 是深拷贝,其实是谬误的,
  • 浅拷贝是按位拷贝对象,它会创立一个新对象,这个对象有着原始对象属性值的一份准确拷贝。如果属性是根本类型,拷贝的就是根本类型的值;如果属性是内存地址(援用类型),拷贝的就是内存地址,因而如果其中一个对象扭转了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(一一成员顺次拷贝),即只复制对象空间而不复制资源。
  • 该办法只拷贝源对象本身的属性,不拷贝其继承的属性。
  • 该办法不会拷贝对象不可枚举的属性
  • undefined 和 null 无奈转成对象,他们不能作为 Object.assign 参数,然而能够作为源对象
  • 属性为 Symbol 的值,能够被该办法拷贝。
  • 浅拷贝,拷贝了第一层的根本数据类型构造,然而深层的仍然没有拷贝到,也就是第一层根本类型数据曾经不会影响了,然而援用却不行,所以还不够

参考 前端进阶面试题具体解答

Array.prototype.silce

看这个办法之前先给大家看看 mdn 对于这个办法的形容。

返回值

返回一个新的数组,蕴含从 start 到 end(不包含该元素)的 arrayObject 中的元素。

阐明

请留神,该办法并不会批改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该应用办法 Array.splice()。

看完它的形容大家就应该差不多明确了吧,让咱们持续用刚刚的例子来实现下

let A = [1,2,3,[4,5]]
let B = A.slice();
B[3] = 4
console.log(A)
console.log(B)
/** 比照 */
[1, 2, 3, [ 4, 5] ]
[1, 2, 3, 4]

能够发现相互不会影响,也就实现了浅拷贝,同理针对于简单的多层数据结构和之前一样也会相互影响,所以集体了解,这个浅字也是由此而来吧,所以下面的说法也不是很筹备,不肯定 AB 相互不影响就肯定是深拷贝了,还得联合数据结构层级来看。

Array.from()

先来看一句 mdn 的形容

Array.from() 办法从一个相似数组或可迭代对象创立一个新的,浅拷贝的数组实例。

Array.form() 用于将两类对象转成真正的数组,一种是 like-array(类数组), 和可遍历的 (iterable) 对象,咱们能够利用这个办法来进行一个浅拷贝。

let A = [1,2,3,[4,5]] ;
let B = Array.from(A) ;
B[3] = 6
console.log(A)
console.log(B)
/** 比照后果 */
[1, 2, 3, [ 4, 5] ]
[1, 2, 3, 6]

能够发现,也是一样的成果,能够实现。

Array.prototype.concat
let A = [1,2,3,[4,5]] ;
let B = [].concat(A) ;
B[3] = 6
console.log(A)
console.log(B)
/** 比照后果 */
[1, 2, 3, [ 4, 5] ]
[1, 2, 3, 6]

数组的办法原理大同小异,适当理解就行,能够自行操作试试看。

ES6 -> []

ES6 的扩大运算符也能够轻松做到,也十分不便来看看吧

let A = [1,2,3,[4,5]]
let B =[...A]
B[3] = 6
console.log(A)
console.log(B)
/** 比照后果 */
[1, 2, 3, [ 4, 5] ]
[1, 2, 3, 6]

扩大运算符是 es6 新增的个性,作用很弱小也十分不便,也是我日常爱用的一种形式,对象,数组都能够操作,除了轻松实现浅拷贝,合并对象也十分的轻松,能够多多应用。

for in

先写个简略版本,因为这个也能够实现深拷贝,所以间接入手吧,

let A = [1,2,3,[4,5]]
let B = []
for (var i in A){B[i] = A[i]
}

B[3] = 9
console.log(A,B)
/** 比照后果 */
[1, 2, 3, [ 4, 5] ]
[1, 2, 3, 9]

发现同样能够实现,原理也很简略,自行剖析下。

浅拷贝的实现有很多种办法,不单单是我这里写出的六种,当然,理论开发中,咱们更重视的是深拷贝,所以咱们来看看如何实现一个深拷贝吧。

深拷贝
JSON.parse(JSon.stringify())
/** 乞丐版本  JSON.parse(JSON.stringify()) */
let A = {
    a: 1,
    b: 2,
    c: [4,5,6]
}
let B = JSON.parse(JSON.stringify(A))
B.a = 2 
B.b = 3
B.c = 4
console.log(A == B)
console.log(A,B)
/** 比照后果  * {a: 1, b: 2, c: [ 4, 5, 6] }  * {a: 2, b: 3, c: 4} */

能够发现,应用这个办法能够做到拷贝之后的 AB 相互不受影响,成为独自一个新值,咱们来剖析下,这个办法外面咱们用到了两个货色,别离是 JSON.stringify()JSON.parse() 这两个办法,首先通过 stringify 将 json 序列化(json 字符串),而后在通过 parse 实现反序列(还原)js 对象,序列化的作用是存储和传输,在这个过程中就会开启新的内存空间就会产生和源对象不一样的空间从而实现深拷贝,理论开发中这个用法曾经能够解决很多场景了,然而仍然有很多弊病。

  • 如果 obj 外面有工夫对象,则 JSON.stringify 后再 JSON.parse 的后果,工夫将只是字符串的模式。而不是工夫对象;
  • 如果 obj 里有 RegExp、Error 对象,则序列化的后果将只失去空对象;
  • 如果 obj 里有函数,undefined,则序列化的后果会把函数或 undefined 失落;
  • 如果 obj 里有 NaN、Infinity 和 -Infinity,则序列化的后果会变成 null
  • JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果 obj 中的对象是有构造函数生成的,应用这个办法后会抛弃对象的 constructor。
  • 该办法不能拷贝 function 类型

综上所看,这个办法也有不少的问题,当然对于一个合格的程序员来说,这个版本也过于 low,咱们当然也心愿实现的更加全面一点。

根底版本(浅拷贝)
/** 根底版本 for  in */

let A = {a: [1, 2, 3],
    b: {a: 1,b: 2},
    c: 99
}

function deepClone(target) {let return_result = {}
    for(let key in target) {return_result[key] = target[key]
    }
    return return_result
}

let B = deepClone(A)
B.a= 99
B.b = 88
console.log(A,'----------',B)
/** 比照后果  *  {a: [ 1, 2, 3], b: {a: 1, b: 2}, c: 99 } *  {a: 99, b: 88, c: 99} */

能够看到,通过 for in 能够实现一个根底的浅拷贝,和 Object.assign() 一样,只能拷贝第一层,然而咱们初步曾经胜利了,

接下来咱们须要思考的是须要思考数组了吧,下面只能是对象,也很简略,咱们只须要加个判断就行,接下来革新一下:

兼容数组(浅拷贝)
/** 根底版本 for  in + 兼容数组 */
let A = [1,2,3,[4,5]]
function deepClone(target) {if (typeof target == 'object'){ // 先判断是不是援用数据类型
        let return_result =  Array.isArray(target) ? [] : {}
        for(let key in target) {return_result[key] = target[key]
        }
        return return_result
    }else{return target;}
}
let B = deepClone(A)
B[3]= 99
B[2] = 88
console.log(A,'----------',B)
/** 比照后果 *  [1, 2, 3, [ 4, 5] ] *  [1, 2, 88, 99] */

能够看到,当初曾经能够兼容数组了,然而仍然不够,咱们仍然只能拷贝第一层,所以接下来须要对深层侧的对象进行递归拷贝了,持续刚刚的办法改良吧:

根底版本 + 兼容数组 + 递归调用(深拷贝)
/** 兼容数组 + 递归调用 */
function deepClone(target) {
    let result;
    if (typeof target === 'object') {if (Array.isArray(target)) {result = []
            for (let i in target) {result.push(deepClone(target[i]))
            }
        } else {result = {}
            for (let key in target) {result[key] = target[key]
            }
        }
    } else {result = target;}
    return result;
}
let A = [1, 2, 3, { a: 1, b: 2}]
let B = deepClone(A)
B[3].a = 99
console.log(A)
console.log(B)
/** 比照后果 *  [1, 2, 3, [ 4, 5] ] *  [1, 2, 3, { a: 99, b: 2} ] */

咱们先判断其类型,再对对象和数组别离递归调用,如果是根本数据类型就间接赋值,至此,咱们曾经能够实现一个根底的深拷贝,然而还远远不够,因为咱们这里只对数组做了类型判断,其余默认都是 object,然而理论状况还会有很多类型,例如,RegExp,Date,Null,Undefined,function 等等很多的类型,所以接下来咱们将其欠缺,加上所以判断,因为类型比拟多,咱们能够把对象的判断独自抽离进去,接下来一起欠缺它吧:在这之前咱们还须要思考的一个点就是 对于 js 的 循环援用问题 当目前的这个办法去拷贝一个带有循环援用关系的对象时是有问题的,来看看:

 /** 根底版本 for  in + 兼容数组 + 递归调用 + 循环援用问题 */
function deepClone(target) {
    let result;
    if (typeof target === 'object') {if (Array.isArray(target)) {result = []
            for (let i in target) {result.push(deepClone(target[i]))
            }
        } else {result = {}
            for (let key in target) {result[key] = target[key]
            }
        }
    } else {result = target;}
    return result;
}
let A = [1, 2, 3, { a: 1, b: 2}]
A[4] = A
let B = deepClone(A)
console.log(A,B)
/**  RangeError: Maximum call stack size exceeded */

会呈现一个超出了最大调用堆栈大小的谬误,这也是深拷贝中的一个坑,在这里咱们能够通过 js 的一种 weakmap 的类型来解决这个问题,通过浏览 mdn 的文档能够理解到:

原生的 WeakMap 持有的是每个键对象的“弱援用”,这意味着在没有其余援用存在时垃圾回收能正确进行。原生 WeakMap 的构造是非凡且无效的,其用于映射的 key 只有在其没有被回收时才是无效的。

正因为这样的弱援用,WeakMap 的 key 是不可枚举的 (没有办法能给出所有的 key)。如果 key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而失去不确定的后果。因而,如果你想要这种类型对象的 key 值的列表,你应该应用 Map

基本上,如果你要往对象上增加数据,又不想烦扰垃圾回收机制,就能够应用 WeakMap。

解决:应用一个 WeakMap 构造存储曾经被拷贝的对象,每一次进行拷贝的时候就先向 WeakMap 查问该对象是否曾经被拷贝,如果曾经被拷贝则取出该对象并返回。

/** 根底版本 for  in + 兼容数组 + 递归调用 + 解决循环援用问题 */
function deepClone(val, hash = new WeakMap()) {if (hash.has(val)) return hash.get()
    let cloneVal;
    if (isObj(val)) { // 判断是不是援用类型
        if (Array.isArray(val)) { // 判断是不是数组 
            cloneVal = []
            hash.set(val, cloneVal)
            for (let i in val) {cloneVal.push(deepClone(val[i]))
            }
        }
        else {cloneVal = {}
            hash.set(val, cloneVal)
            for (let key in val) {cloneVal[key] = val[key]
            }
        }
    } else {cloneVal = val;}
    return cloneVal;
}
/** 是否是援用类型 */
function isObj(val) {return (typeof val == 'object' || typeof val == 'function') && val != null
}
var a = {}
a.a = a
var b = deepClone(a)
console.log(b)

这样就能够初步解决循环调用问题,接下来要思考的是如何为更多类型做不同解决,咱们借用之前的一个检测 js 类型的文章,通过 js 检测数据类型 的这个办法来为多种类型别离解决。

/** 残缺版本 */
function deepClonea(val, map = new WeakMap()) {let type = getType(val); // 当是援用类型的时候先拿到其确定的类型
    if (isObj(val)) {switch (type) {
            case 'date':                   // 日期类型从新 new 一次传入之前的值,date 实例化自身后果不变
                return new Date(val);
                break;
            case 'regexp':                 // 正则类型间接 new 一个新的正则传入 source 和 flags 即可
                return new RegExp(val.source, val.flags);
                break;
            case 'function':               // 如果是函数类型就间接通过 function 包裹返回一个新的函数,并且扭转 this 指向
                return new RegExp(val.source, val.flags);
                break;
            default:
                let cloneVal = Array.isArray(val) ? [] : {};
                if (map.has(val)) return map.get(val)
                map.set(val, cloneVal)
                for (let key in val) {if (val.hasOwnProperty(key)) { // 判断是不是本身的 key
                        cloneVal[key] = deepClone(val[key]), map;// 每一项就算是根本类型也须要走 deepclone 办法进行拷贝
                    }
                }
                return cloneVal;
        }
    } else {return val;     // 当是根本数据类型的时候间接返回}
}
function isObj(val) {   // 判断是否是援用类型
    return (typeof val == 'object' || typeof val == 'function') && val != null
}
function getType(data) { // 获取类型
    var s = Object.prototype.toString.call(data);
    return s.match(/\[object (.*?)\]/)[1].toLowerCase();};
// /** 测试 */
var a = {}
a.a = a
var b = deepClonea(a)
console.log(b)
最终完整版

下面差不多曾经实现了一个能够应答大部分场景的深拷贝了,上面让咱们用 class 类的办法来革新一下,不便前期对其进行扩大更改。

/**  改用 class 类写 */
class DeepClone {constructor(){cloneVal: null;}
    clone(val, map = new WeakMap()) {let type = this.getType(val); // 当是援用类型的时候先拿到其确定的类型
        if (this.isObj(val)) {switch (type) {
                case 'date':             // 日期类型从新 new 一次传入之前的值,date 实例化自身后果不变
                    return new Date(val);
                    break;
                case 'regexp':           // 正则类型间接 new 一个新的正则传入 source 和 flags 即可
                    return new RegExp(val.source, val.flags);
                    break;
                case 'function':        // 如果是函数类型就间接通过 function 包裹返回一个新的函数,并且扭转 this 指向
                    return new RegExp(val.source, val.flags);
                    break;
                default:
                    this.cloneVal = Array.isArray(val) ? [] : {};
                    if (map.has(val)) return map.get(val)
                    map.set(val, this.cloneVal)
                    for (let key in val) {if (val.hasOwnProperty(key)) { // 判断是不是本身的 key
                            this.cloneVal[key] = new DeepClone().clone(val[key], map);
                        }
                    }
                    return this.cloneVal;
            }
        } else {return val;     // 当是根本数据类型的时候间接返回}
    }
    /** 判断是否是援用类型 */
    isObj(val) {return (typeof val == 'object' || typeof val == 'function') && val != null
    }
    /** 获取类型 */
    getType(data) {var s = Object.prototype.toString.call(data);
        return s.match(/\[object (.*?)\]/)[1].toLowerCase();};
}
 /** 测试 */
var a ={
    a:1,
    b:true,
    c:undefined,
    d:null,
    e:function(a,b){return a + b},
    f: /\W+/gi,
    time: new Date(),}
const deepClone = new DeepClone()
let b = deepClone.clone(a)
console.log(b)

好了下面就是本次总结的深拷贝,当然还不够欠缺,还有很多种场景,前期可能会补充,然而这个目前曾经能够应答你很大一部分的场景了。

正文完
 0