机试败在了深浅拷贝上

112 阅读8分钟

前言

事情是这样的,最近参加了华为OD的机考,题目难度还行。看完三道题目,我抱着九分的信心能都做出来。结果在最后一道题,卡在了如何实现深浅拷贝的问题上。深浅拷贝问题也是前端面试中经常会被问到的一个问题,因此痛定思痛,要把此文总结出来,以加深自己对深浅拷贝的理解和应用。

机试题目

题目描述:有一堆任务序列,给出每个任务的开始事件和结束事件以及占用的服务器,求执行完这些任务最少需要多少个服务器。

例:给出3个任务序列

[[1,4,3],[3,5,2],[6,7,3]]

那么完成这个任务序列最少要5个服务器。

解释: 任务1在时间1到4使用了3个服务,任务2在时间3到5使用2个服务器,这两个任务要同时进行最少需要5个服务器。而任务3执行时间为6到7,最少只需要3个服务器。那么任务序列完成最少需要5个服务器。

解题思路: 一个字典来进行每一步计算结果的存放。遍历循环所有的任务,将每一步循环的对象的任务起始时间作为key放在字段中,需要的服务器数作为值。然后每次循环中再循环我们的字典,对比当前对象和字典循环对象的起始时间,如果相交,则加上该服务器数。最后字典所有键值对中的值最大值就是最少需要的服务器数了。

遇到的问题: 在将对象存入字典时,是通过赋值进行的。因此在进行加法计算时,原数组的值也被修改了。在下一次循环进行计算时的值是上一次修改的结果,因此导致最终结果出现问题。

细说深浅拷贝

堆、栈

栈:自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址

堆:动态分配的内存,大小不定,也不会自动释放。里面存放引用类型的值。

数据类型

数据类型分为基本数据类型对象数据类型:

(1)基本数据类型的特定:直接存储在中的数据。

(2)引用数据类型的特点:存储的是该对象在栈中的引用(指针),真实的数据存放在内存里。栈中的指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索在栈中的地址,取得地址后从堆中获取实体。

(3)基本类型与引用类型的区别:最大区别实际就是传值与传址的区别。 ①值传递:基本类型采用的是值传递。 ②址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量。

深拷贝、浅拷贝

浅拷贝---浅拷贝是指复制对象的时候,只对第一层键值对进行独立的复制,如果对象内还有对象,则只能复制嵌套对象的地址

深拷贝---深拷贝是指复制对象的时候完全的拷贝一份对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。其实只要递归下去,把那些属性的值仍然是对象的再次进入对象内部一 一进行复制即可。

深浅拷贝是只针对Object和Array遮掩的引用数据类型,会另外创建一个一摸一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。 浅拷贝只复制指向某个对象的指针,而不是复制对象本身,新旧对象还是共享同一块内存.

赋值和浅拷贝的区别

赋值:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的地址,而不是堆中的数据。即两个对象指向的是同一个存储空间,当一个对象改变时,另一个对象也会改变。

浅拷贝:浅拷贝是按位拷贝对象,会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。 ①属性是基本类型,拷贝的是基本类型的值; ②属性是引用类型(内存地址),拷贝的就是内存地址; 如果其中一个对象改变这个地址,就会影响到另一个对象,即默认拷贝构造函数只是对对象进行浅拷贝复制。

console.log('引用类型赋值 const');
const j1={name:'lyj'}
console.log('j',j1);
const y1=j1;
console.log('y',y1);
 y1.name='666'
 console.log('j',j1,'y',y1);
 console.log('------------------------');
 /**
  * 引用类型赋值 const
    j { name: 'lyj' }
    y { name: 'lyj' }
    j { name: '666' } y { name: '666' }
  */

console.log('浅拷贝')
let obj={name:'lll'}
function shalldowCopy(src){
    let obj2={}; //一个新的对象,即创建了一个新的块
    for (let prop in src){
        if(src.hasOwnProperty(prop)){
            obj2[prop]=src[prop];
        }
    }
    return obj2;
}
let obg66=shalldowCopy(obj);
console.log('原数据',obj);
console.log('拷贝结果',obg66);
obg66.name='222'
console.log('更改浅拷贝过来的数据','拷贝得来的数据',obg66,'原数据',obj);

/**
 * 浅拷贝
    原数据 { name: 'lll' }
    拷贝结果 { name: 'lll' }
    更改浅拷贝过来的数据 拷贝得来的数据 { name: '222' } 原数据 { name: 'lll' }
 */

浅拷贝方法

(1)Object.assign()

    // 浅拷贝的首先方式
    // Object.assign()
    //此方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
    // 拷贝的是对象的属性的引用,而不是对象本身
    console.log('------------------------');

    let o1={a:{a:'a'},b:'b'}
    let o2=Object.assign({},o1);//assign(target,source),source会加载target里,如果有相同属性会被替代,没有则会往里加,多个source的话后面的会覆盖前面的
    console.log('assign浅拷贝,原数据',o1,'拷贝结果',o2);
    o2.a.a='a1';
    console.log('更改拷贝a.a数据,原数据',o1,'拷贝结果',o2);
    o2.b='b1'
    console.log('更改拷贝b数据,原数据',o1,'拷贝结果',o2);

    /**
     * assign浅拷贝,原数据 { a: { a: 'a' }, b: 'b' } 拷贝结果 { a: { a: 'a' }, b: 'b' }
    更改拷贝a.a数据,原数据 { a: { a: 'a1' }, b: 'b' } 拷贝结果 { a: { a: 'a1' }, b: 'b' }
    更改拷贝b数据,原数据 { a: { a: 'a1' }, b: 'b' } 拷贝结果 { a: { a: 'a1' }, b: 'b1' }
     */
    //结果对象中的引用类型被改变了,而基本类型没有
    //原因,浅拷贝拷贝到的是对象的引用(即指针指向的堆中的值,但是对象中的属性为对象时,获取到的堆中的值是个指针指向了对应的引用地址)
(2)Array.prototype.concat()

// Array.prototype.concat()
let o1=[1,2,{name:'lll'}];
let o2=o1.concat();
console.log('assign浅拷贝,原数据',o1,'拷贝结果',o2);
o2[2].name='a1';
console.log('更改拷贝l1[2]数据,原数据',o1,'拷贝结果',o2);
o2[0].name='a1';
console.log('更改拷贝l1[0]数据,原数据',o1,'拷贝结果',o2);
/**
 * assign浅拷贝,原数据 [ 1, 2, { name: 'lll' } ] 拷贝结果 [ 1, 2, { name: 'lll' } ]
    更改拷贝l1[2]数据,原数据 [ 1, 2, { name: 'a1' } ] 拷贝结果 [ 1, 2, { name: 'a1' } ]
    更改拷贝l1[0]数据,原数据 [ 1, 2, { name: 'a1' } ] 拷贝结果 [ 1, 2, { name: 'a1' } ]

 */

(3)Array.prototype.slice()
// Array.prototype.concat()
let o1=[1,2,{name:'lll'}];
let o2=o1.slice();
console.log('assign浅拷贝,原数据',o1,'拷贝结果',o2);
o2[2].name='a1';
console.log('更改拷贝l1[2]数据,原数据',o1,'拷贝结果',o2);
o2[0].name='a1';
console.log('更改拷贝l1[0]数据,原数据',o1,'拷贝结果',o2);
/**
 * 
assign浅拷贝,原数据 [ 1, 2, { name: 'lll' } ] 拷贝结果 [ 1, 2, { name: 'lll' } ]
更改拷贝l1[2]数据,原数据 [ 1, 2, { name: 'a1' } ] 拷贝结果 [ 1, 2, { name: 'a1' } ]
更改拷贝l1[0]数据,原数据 [ 1, 2, { name: 'a1' } ] 拷贝结果 [ 1, 2, { name: 'a1' } ]
 */

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

(1)如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。 (2)对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

深拷贝
(1)序列化 JSON.parse(JSON.stringify())

原理:用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈。 缺点:这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。因为JSON.stringify()方法是将一个JS值(对象或数组)转换成一个JSON字符串,不能接受函数。

(2)手写递归方法

原理:遍历对象,数组直到里面都是基本数据类型,然后再去赋值,就是深度拷贝。

(3)函数库lodash
let o3={a:1,c:{a:{c:[1,2,3]}}};
let o4=_.cloneDeep(o3);
console.log('原对象和深拷贝对象对比c.a',o3.c.a==o4.c.a);
// 这对比的其实是引用(地址),而不是值,所以不一致

总结

和原数据是否指向同一对象第一层数据为基本数据类型原数据中包含了子对象
赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变不会使原数据一同改变改变会使原数据一同改变
深拷贝改变不会使数据一同改变改变不会使原数据一同改变