这是前端面试题系列的第 9 篇,你可能错过了前面的篇章,能够在这里找到:前端
面试的时候,我常常会问候选人深拷贝与浅拷贝的问题。由于它能够考察一我的的不少方面,好比基本功,逻辑能力,编码能力等等。node
另外在实际工做中,也常会遇到它。好比用于页面展现的数据状态,与须要传给后端的数据包中,有部分字段的值不一致的话,就须要在传参时根据接口文档覆写那几个字段的值。面试
最多见的可能就是 status 这个参数了。界面上的展现须要 Boolean 值,然后端同窗但愿拿到的是 Number 值,1 或者 0。为了避免影响展现效果,每每就须要深拷贝一下,再进行覆写,不然界面上就会由于某些值的变化,出现奇怪的现象。segmentfault
至于为何会这样,下文会讲到。立刻开始今天的主题,让咱们先从赋值开始提及。后端
Javascript 的原始数据类型有这几种:Boolean、Null、Undefined、Number、String、Symbol(ES6)
。它们的赋值很简单,且赋值后两个变量互不影响。数组
let test1 = 'chao'; let test2 = test1; // test2: chao test1 = 'chao_change'; // test2: chao // test1: chao_change
另外的引用数据类型有:Object
和 Array
。深拷贝与浅拷贝的出现,就与这两个数据类型有关。微信
const obj = {a:1, b:2}; const obj2 = obj; obj2.a = 3; console.log(obj.a); // 3
依照赋值的思路,对 Object 引用类型进行拷贝,就会出问题。不少状况下,这不是咱们想要的。这时,就须要用浅拷贝来实现了。前端工程师
什么是浅拷贝?能够这么理解:建立一个新的对象,把原有的对象属性值,完整地拷贝过来。其中包括了原始类型的值,还有引用类型的内存地址。框架
让咱们用 Object.assign
来改写一下上面的例子:函数
const obj = {a:1, b:2}; const obj2 = Object.assign({}, obj); obj2.a = 3; console.log(obj.a); // 1
Ok,改变了 obj2 的 a 属性,但 obj 的 a 并无发生变化,这正是咱们想要的。
但是,这样的拷贝还有瑕疵,再改一下例子:
const arr = [{a:1,b:2}, {a:3,b:4}]; const newArr = [].concat(arr); newArr.length = 1; // 为了方便区分,只保留新数组的第一个元素 console.log(newArr); // [{a:1,b:2}] console.log(arr); // [{a:1,b:2},{a:3,b:4}] newArr[0].a = 123; // 修改 newArr 中第一个元素的a console.log(arr[0]); // {a: 123, b: 2},居然把 arr 的第一个元素的 a 也改了
oh,no!这不是咱们想要的...
通过一番查找,才发现:原来,对象的 Object.assign()
,数组的 Array.prototype.slice()
和 Array.prototype.concat()
,还有 ES6 的 扩展运算符
,都有相似的问题,它们都属于 浅拷贝。这一点,在实际工做中处理数据的组装时,要格外注意。
因此,我将浅拷贝这样定义:只拷贝第一层的原始类型值,和第一层的引用类型地址
。
咱们固然但愿当拷贝多层级的对象时,也能实现互不影响的效果。因此,深拷贝的概念也就油然而生了。我将深拷贝定义为:拷贝全部的属性值,以及属性地址指向的值的内存空间
。
也就是说,当遇到对象时,就再新开一个对象,而后将第二层源对象的属性值,完整地拷贝到这个新开的对象中。
按照浅拷贝的思路,很容易就想到了递归调用。因此,就本身封装了个深拷贝的方法:
function deepClone(obj) { if(!obj && typeof obj !== 'object'){ return; } var newObj= toString.call(obj) === '[object Array]' ? [] : {}; for (var key in obj) { if (obj[key] && typeof obj[key] === 'object') { newObj[key] = deepClone(obj[key]); } else { newObj[key] = obj[key]; } } return newObj; }
再试试看:
let arr = [{a:1,b:2}, {a:3,b:4}]; let newArr = deepClone(arr); newArr.length = 1; // 为了方便区分,只保留新数组的第一个元素 console.log(newArr); // [{a:1, b:2}] console.log(arr); // [{a:1, b:2}, {a:3, b:4}] newArr[0].a = 123; // 修改 newArr 中第一个元素的 a console.log(arr[0]); // {a:1, b:2}
ok,这下搞定了。
不过,这个方法貌似会存在 引用丢失 的的问题。好比这样:
var b = {}; var a = {a1: b, a2: b}; a.a1 === a.a2 // true var c = clone(a); c.a1 === c.a2 // false
若是咱们的需求是,应该丢失引用,那就能够用这个方法。反之,就得想办法解决。
固然,还有最简单粗暴的深拷贝方法,就是利用 JSON
了。像这样:
let newArr2 = JSON.parse(JSON.stringify(arr)); console.log(arr[0]); // {a:1, b:2} newArr2[0].a = 123; console.log(arr[0]); // {a:1, b:2}
可是,JSON 内部用了递归的方式。数据一但过多,就会有递归爆栈的风险。
// Maximum call stack size exceeded
有位大佬给出了深拷贝的终极方案,利用了“栈”的思想。
function cloneForce(x) { // 用来去重 const uniqueList = []; let root = {}; // 循环数组 const loopList = [ { parent: root, key: undefined, data: x, } ]; while(loopList.length) { // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,不然拷贝到子元素 let res = parent; if (typeof key !== 'undefined') { res = parent[key] = {}; } // 数据已经存在 let uniqueData = uniqueList.find((item) => item.source === data ); if (uniqueData) { parent[key] = uniqueData.target; // 中断本次循环 continue; } // 数据不存在 // 保存源数据,在拷贝数据中对应的引用 uniqueList.push({ source: data, target: res, }); for(let k in data) { if (data.hasOwnProperty(k)) { if (typeof data[k] === 'object') { // 下一次循环 loopList.push({ parent: res, key: k, data: data[k], }); } else { res[k] = data[k]; } } } } return root; }
其思路是:引入一个数组 uniqueList
用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在 uniqueList
中了,若是在的话就不执行拷贝逻辑了。
这个方法是在解决递归爆栈问题的基础上,加以改进解决循环引用的问题。但若是你并不想保持引用,那就改用 cloneLoop
(用于解决递归爆栈)便可。有兴趣的同窗,能够前往 深拷贝的终极探索(90%的人都不知道),查看更多的细节。
所谓深拷贝与浅拷贝,指的是 Object
和 Array
这样的引用数据类型。
浅拷贝
,只拷贝第一层的原始类型值,和第一层的引用类型地址。
深拷贝
,拷贝全部的属性值,以及属性地址指向的值的内存空间。经过递归调用,或者 JSON 来作深拷贝,都会有一些问题。而 cloneForce 方法却是目前看来最完美的解决方案了。
在平常的工做中,咱们要特别注意,对象的 Object.assign()
,数组的 Array.prototype.slice()
和 Array.prototype.concat()
,还有 ES6 的 扩展运算符
,都属于浅拷贝。当须要作数据组装时,必定要用深拷贝,以避免影响界面展现效果。
莉莉丝游戏招 中高级前端工程师
啦!!!
你玩过《小冰冰传奇([刀塔传奇])》么?你玩过《剑与家园》么?
你想和 薛兆丰老师 成为同事么?有兴趣的同窗,能够 关注下面的公众 号加我微信 详聊哈~