在實(shí)際開發(fā)中,我們經(jīng)常會(huì)遇到原對象不能修改,但是又需要用到里面的數(shù)據(jù),比如原對象里面的值是一個(gè)數(shù)組,我們要將他拼接成一個(gè)字符串。
這個(gè)時(shí)候如果可以深拷貝一個(gè)對象,用拷貝出來的這個(gè)對象進(jìn)行操作就會(huì)非常方便,這也是前端面試中,經(jīng)常會(huì)遇到的題,可以考驗(yàn)候選人,對于js的知識(shí)是否掌握的過硬。
對象的拷貝,可以分為深拷貝和淺拷貝。
目前自己總結(jié)的對象拷貝的方法:
- 直接賦值 obj2 = obj1
- Object對象上的:Object.assign() 方法
- JSON 對象上的:JSON.parse(JSON.stringify(obj)) 方法
- for in 循環(huán)遍歷對象的每一個(gè)鍵
- 更復(fù)雜的自定義方法
- 第三方庫:比如 lodash 的cloneDeep
淺拷貝
很多情況我們只要實(shí)現(xiàn)對象的淺拷貝就好了,并不需要真的進(jìn)行深拷貝,但是在進(jìn)行淺拷貝的時(shí)候,一定是經(jīng)過思考的,比如我們就想用里面的簡單數(shù)據(jù)類型而已,不會(huì)對里面的引用值進(jìn)行修改,否則運(yùn)行結(jié)果可能會(huì)在你的意料之外。
一、直接賦值 obj2 = obj1
由于對象的值是存在內(nèi)存的堆中的,直接賦值方法,新的對象指向的是原對象在內(nèi)存中的地址而已,這個(gè)時(shí)候,改變新對象的屬性值,會(huì)通過引用地址去找到內(nèi)存中的真正的值。導(dǎo)致原對象的值也就改變了,這個(gè)直接賦值的方法,真的就是原對象多了個(gè)名字而已,就好像這個(gè)人有個(gè)原名字叫 李雷,你又給他起了個(gè)名字叫 小雷。
二、 Object對象上的:Object.assign() 方法
MDN上這樣介紹Object.assign(),'Object.assign() 方法用于將所有可枚舉的屬性的值從一個(gè)或多個(gè)源對象復(fù)制到目標(biāo)對象。它將返回目標(biāo)對象',好吧,并看不出是深拷貝還是淺拷貝,我們來測試一下
let srcObj = {'name': 'lilei', 'age': '20'};
let copyObj2 = Object.assign({}, srcObj, {'age': '21'});
copyObj2.age = '23';
console.log('srcObj', srcObj); //{ name: 'lilei', age: '22' }
看起來好像是深拷貝了,那其實(shí)這里let copyObj2 = Object.assign({}, srcObj, {'age': '21'}); 我們把srcObj 給了一個(gè)新的空對象。同樣目標(biāo)對象為 {},我們再來測試下:
srcObj = {'name': '明', grade: {'chi': '50', 'eng': '50'} };
copyObj2 = Object.assign({}, srcObj);
copyObj2.name = '紅';
copyObj2.grade.chi = '60';
console.log('新 objec srcObj', srcObj); // { name: '明', grade: { chi: '60', eng: '50' } }
從例子中可以看出,改變復(fù)制對象的name 和 grade.chi ,源對象的name沒有變化,但是grade.chi卻被改變了。因此我們可以看出Object.assign()拷貝的只是屬性值,假如源對象的屬性值是一個(gè)指向?qū)ο蟮囊?,它也只拷貝那個(gè)引用值。
也就是說,對于Object.assign()而言, 如果對象的屬性值為簡單類型(string, number),通過Object.assign({},srcObj);得到的新對象為‘深拷貝’;如果屬性值為對象或其它引用類型,那對于這個(gè)對象而言其實(shí)是淺拷貝的。這是Object.assign()特別值得注意的地方。
多說一句,Object.assign({}, src1, src2); 對于scr1和src2之間相同的屬性是直接覆蓋的,如果屬性值為對象,是不會(huì)對對象之間的屬性進(jìn)行合并的。
- 源于Object.assign() 方法——原作者地址:javascript對象的淺拷貝、深拷貝和Object.assign方法淺析
三、JSON 對象上的:JSON.parse(JSON.stringify(obj)) 方法
摘選
JSON.parse(JSON.stringify(obj))一般用來實(shí)現(xiàn)深拷貝:
1. JSON.parse()將對象序列化(JSON字符串);
2. JSON.string()實(shí)現(xiàn)反序列化(還原)js對象
復(fù)制代碼
注意一、obj里面有時(shí)間對象,則JSON.stringiify后再JSON.parse的結(jié)果,時(shí)間將只是字符串的形式。而不是時(shí)間對象
var test = {
name: 'a',
date: [new Date(1536627600000), new Date(1540047600000)],
}
let b;
b = JSON.parse(JSON.stringify(test))
1. console.log(b)
// b = {
// name: "a",
// data: [
// "2018-09-11T01:00:00.000Z",
// "2018-10-20T15:00:00.000Z"
// ]
// }
(typeof b.date[0]) => string
2. console.log(test)
// test = {
// name: "a",
// data: [
// "Tue Sep 11 2018 09:00:00 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間)",
// "Sat Oct 20 2018 23:00:00 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間)",
// ]
// }
(typeof test.date[0]) => object
注意二、如果obj里有RegExp、Error對象,則序列化的結(jié)果將只得到空對象
name: "a",
date: new RegExp('\w+')
}
const copyed = JSON.parse(JSON.stringify(test));
test.name = 'test';
console.error('ddd', test, copyed)
1. console.log(copyed)
// copyed = {
// name: "a",
// data: {}
// }
2. console.log(test)
// test = {
// name: "test",
// data: new RegExp('\w+')
// }
注意三、如果obj里有函數(shù),undefined,則序列化的結(jié)果會(huì)把函數(shù)或 undefined丟失;
const test = {
name: 'a',
date: function foo(){
console.log('haha')
},
a: undefined
}
const copyed = JSON.parse(JSON.stringify(test))
1. console.log(copyed)
// copyed = {
// name: "a"
// }
2. console.log(test)
// test = {
// name: "test",
// date: function foo(){
// console.log('haha')
// },
// a: undefined
// }
注意四、如果obj里有NaN、Infinity和-Infinity,則序列化的結(jié)果會(huì)變成null
// test = {
// name: "test",
// date: null // 即 NaN返回null
// }
注意五、JSON.stringify()只能序列化對象的可枚舉的自有屬性,例如 如果obj中的對象是有構(gòu)造函數(shù)生成的,則使用JSON.parse(JSON.stringify(obj))深拷貝后,會(huì)丟棄對象的constructor;
function Person(name) {
this.name = name;
// console.log(name)
}
const liai = new Person('liai');
const test = {
name: 'a',
date: liai,
};
const copyed = JSON.parse(JSON.stringify(test));
test.name = 'test'
console.log(test, copyed)
1. console.log(copyed)
// copyed = {
// name: "a",
// data: {name: "liai"}
// }
2. console.log(test)
// test = {
// name: "test",
// data: Person{name: "liai"}
// }
復(fù)制代碼
注意六、如果對象中存在循環(huán)引用的情況也無法正確實(shí)現(xiàn)深拷貝;
- 關(guān)于JSON.parse(JSON.stringify(obj))方法原作者地址:https://juejin.im/post/5df0545ee51d4557ed542146
四、for in 循環(huán)遍歷對象的每一個(gè)鍵
簡單的for in也是只能實(shí)現(xiàn)對象的一級(jí),簡單數(shù)據(jù)的(字符串 、數(shù)字、布爾值、對空(Null)、未定義(Undefined)、Symbol)淺拷貝,復(fù)雜數(shù)據(jù)比如 值里面存的是一個(gè)對象,function 、array 那就無法深度拷貝。
- 我們可以用遞歸 + for in 來實(shí)現(xiàn)一個(gè)日常開發(fā)中可以足夠用的淺拷貝
(1)//自定義的復(fù)制方法
function clone(obj) {
var copy = {};
for (var attr in obj) {
copy[attr] = typeof(obj[attr])==='object' ? clone(obj[attr]) : obj[attr];
}
return copy;
}
//測試樣例
var a = {v1:1, v2:2};
var b = clone(a);
b.v1 = 3;
console.log("對象a:",a);
console.log("對象b:",b);
(2)也可以直接給 Object 增加個(gè) clone 方法,其內(nèi)部實(shí)現(xiàn)原來同上面是一樣的。
//自定義的復(fù)制方法
Object.prototype.clone = function() {
var copy = (this instanceof Array) ? [] : {};
for (var attr in this) {
if (this.hasOwnProperty(attr)){
copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];
}
}
return copy;
};
//測試樣例
var a = {v1:1, v2:2};
var b = a.clone();
b.v1 = 3;
console.log("對象a:",a);
console.log("對象b:",b);
深拷貝
前面四個(gè)方法都無法實(shí)現(xiàn)深拷貝啊,那自己實(shí)現(xiàn)一個(gè)深拷貝真的有那么難嗎?
五、 更復(fù)雜的自定義方法
function deepClone(data) {
const type = this.judgeType(data);
let obj;
if (type === 'array') {
obj = [];
} else if (type === 'object') {
obj = {};
} else {
// 不再具有下一層次
return data;
}
if (type === 'array') { // eslint-disable-next-line
for (let i = 0, len = data.length; i < len; i++) {
obj.push(this.deepClone(data[i]));
}
} else if (type === 'object') { // 對原型上的方法也拷貝了....
// eslint-disable-next-line
for (const key in data) {
obj[key] = this.deepClone(data[key]);
}
}
return obj;
}
function judgeType(obj) {
// tostring會(huì)返回對應(yīng)不同的標(biāo)簽的構(gòu)造函數(shù)
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object',
};
if (obj instanceof Element) {
return 'element';
}
return map[toString.call(obj)];
}
作者:MepJia
鏈接:https://juejin.im/post/5df0545ee51d4557ed542146
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
六、第三方庫:比如 lodash 的cloneDeep,
不想自己寫就用這個(gè),100多M的庫,不用的擔(dān)心,這是開發(fā)依賴而已,只在開發(fā)的時(shí)候用到這個(gè)庫,上線時(shí)候會(huì)編譯掉的。
官網(wǎng):https://lodash.com/
- 如果用jQuery
jQuery 自帶的 extend 方法可以用來實(shí)現(xiàn)對象的復(fù)制。
var a = {v1:1, v2:2};
var b = {};
$.extend(b,a);
b.v1 = 3;
console.log("對象a:",a);
console.log("對象b:",b);