老生常談JavaScript深拷貝與淺拷貝

1 淺拷貝概念

深拷貝和淺拷貝是隻針對Object和Array這樣的引用數據類型的。

淺拷貝是創建一個新對象,該對象有著原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型,拷貝的就是內存地址 ,所以如果其中一個對象改變瞭這個地址,就會影響到另一個對象

示例代碼:

let people = { //定義一個People對象
    name: "張三",
    age: 3,
    address: "中國"
}
console.log("原對象:", people);
let newPeople = people; //進行淺拷貝
console.log("新對象:", newPeople);
//原對象: { name: '張三', age: 3, address: '中國' }
//新對象: { name: '張三', age: 3, address: '中國' }
//為對象修改名字
newPeople.name = "橘貓吃不胖";
console.log("原對象:", people);
console.log("新對象:", newPeople);
//原對象: { name: '橘貓吃不胖', age: 3, address: '中國' }
//新對象: { name: '橘貓吃不胖', age: 3, address: '中國' }

從上面的示例可以看出,當newPeople的name屬性修改後,原來的people也發生瞭變化,這是因為新創建的對象與舊對象具有相同的內存地址

2 深拷貝概念

深拷貝是將一個對象從內存中完整的拷貝一份出來,從堆內存中開辟一個新的區域存放新對象,且修改新對象不會影響原對象

示例代碼:

let people = { //定義一個People對象
    name: "張三",
    age: 3,
    address: "中國"
}
//對people進行深拷貝
let newPeople = JSON.parse(JSON.stringify(people));
console.log("原對象:", people);
console.log("新對象:", newPeople);
// 原對象: { name: '張三', age: 3, address: '中國' }
// 新對象: { name: '張三', age: 3, address: '中國' }
//修改新對象中的adress屬性
newPeople.address = "俄羅斯";
console.log("原對象:", people);
console.log("新對象:", newPeople);
// 原對象: { name: '張三', age: 3, address: '中國' }
// 新對象: { name: '張三', age: 3, address: '俄羅斯' }

從上面的例子可以看出,深拷貝後,修改新對象,不會影響原對象。

3 淺拷貝的實現方式

3.1 Object.assign()

Object.assign()方法用於將所有可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。Object.assign() 進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象本身。

語法:

Object.assign(target, ...sources)
//target:目標對象;sources:源對象。

如果目標對象中的屬性具有相同的鍵,則屬性將被源對象中的屬性覆蓋。後面的源對象的屬性將類似地覆蓋前面的源對象的屬性。

示例:

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
//進行淺拷貝
const returnedTarget = Object.assign(target, source);
console.log(target);
console.log(returnedTarget);
// { a: 1, b: 4, c: 5 }
// { a: 1, b: 4, c: 5 }
//修改其中的值
target.b = 10;
console.log(target);
console.log(returnedTarget);
// { a: 1, b: 10, c: 5 }
// { a: 1, b: 10, c: 5 }

當對象object隻有一層的時候,是深拷貝,示例代碼如下:

const obj = { name: "橘貓吃不胖" };
//進行淺拷貝
let newObj = Object.assign({}, obj);
//修改新對象中的name屬性為張三
newObj.name = "張三";
console.log("原對象:", obj);
console.log("新對象:", newObj);
// 原對象: { name: '橘貓吃不胖' }
// 新對象: { name: '張三' }

3.2 Array.prototype.concat()

concat()方法用於合並兩個或多個數組。此方法不會更改現有數組,而是返回一個新數組。

語法:

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
//valueN可選,數組和/或值,將被合並到一個新的數組中。
//如果省略瞭所有 valueN 參數,則 concat 會返回調用此方法的現存數組的一個淺拷貝。

示例代碼:

let arr1 = [1, 2, { name: "橘貓吃不胖" }];
//進行淺拷貝
let arr2 = arr1.concat();
console.log("原數組:", arr1);
console.log("新數組:", arr2);
// 原數組: [ 1, 2, { name: '橘貓吃不胖' } ]
// 新數組: [ 1, 2, { name: '橘貓吃不胖' } ]
//修改原數組
arr1[1] = "hhhhh";
console.log("原數組:", arr1);
console.log("新數組:", arr2);
// 原數組: [ 1, 'hhhhh', { name: '橘貓吃不胖' } ]
// 新數組: [ 1, 2, { name: '橘貓吃不胖' } ]

3.3 Array.prototype.slice()

slice() 方法返回一個新的數組對象,這一對象是一個由 begin 和 end 決定的原數組的淺拷貝(包括 begin,不包括end)。原始數組不會被改變。

語法:

arr.slice([begin[, end]])
//begin:可選,提取起始處的索引(從 0 開始),從該索引開始提取原數組元素。
//如果該參數為負數,則表示從原數組中的倒數第幾個元素開始提取,slice(-2) 表示提取原數組中的倒數第二個元素到最後一個元素(包含最後一個元素)。
//如果省略 begin,則 slice 從索引 0 開始。
//如果 begin 超出原數組的索引范圍,則會返回空數組。
//end:可選,提取終止處的索引(從 0 開始),在該索引處結束提取原數組元素。slice 會提取原數組中索引從 begin 到 end 的所有元素(包含 begin,但不包含 end)。
//slice(1,4) 會提取原數組中從第二個元素開始一直到第四個元素的所有元素 (索引為 1, 2, 3的元素)。
//如果該參數為負數, 則它表示在原數組中的倒數第幾個元素結束抽取。 slice(-2,-1) 表示抽取瞭原數組中的倒數第二個元素到最後一個元素(不包含最後一個元素,也就是隻有倒數第二個元素)。
//如果 end 被省略,則 slice 會一直提取到原數組末尾。
//如果 end 大於數組的長度,slice 也會一直提取到原數組末尾。

示例代碼:

let arr1 = [1, 2, { name: "橘貓吃不胖" }];
//進行淺拷貝
let arr2 = arr1.slice();
console.log("原數組:", arr1);
console.log("新數組:", arr2);
// 原數組: [ 1, 2, { name: '橘貓吃不胖' } ]
// 新數組: [ 1, 2, { name: '橘貓吃不胖' } ]
//修改原數組
arr1[1] = "hhhhh";
console.log("原數組:", arr1);
console.log("新數組:", arr2);
// 原數組: [ 1, 'hhhhh', { name: '橘貓吃不胖' } ]
// 新數組: [ 1, 2, { name: '橘貓吃不胖' } ]

3.4 直接賦值

直接使用“=”賦值可以實現淺拷貝,示例代碼如下:

let obj1 = { //定義一個對象obj1
    name: "張三",
    age: 34
}
let obj2 = obj1; //進行淺拷貝
console.log("obj1:", obj1);
console.log("obj2:", obj2);
// obj1: { name: '張三', age: 34 }
// obj2: { name: '張三', age: 34 }
//修改obj2中的name屬性
obj2.name = "橘貓吃不胖";
console.log("obj1:", obj1);
console.log("obj2:", obj2);
// obj1: { name: '橘貓吃不胖', age: 34 }
// obj2: { name: '橘貓吃不胖', age: 34 }

4 深拷貝的實現方式

4.1 JSON.parse(JSON.stringify())

JSON是一種語法,用來序列化對象、數組、數值、字符串、佈爾值和 null 。它基於JavaScript語法,但與之不同:JavaScript不是JSON,JSON也不是JavaScript。

JSON對象包含兩個方法:用於解析JSON的parse()方法,以及將對象/值轉換為JSON字符串的stringify()方法,下面對這兩種方法進行一些介紹。

JSON.parse()方法用來解析JSON字符串,構造由字符串描述的JavaScript值或對象。提供可選的 reviver 函數用以在返回之前對所得到的對象執行變換(操作)。

語法:

JSON.parse(text[, reviver])
//text:要被解析成 JavaScript 值的字符串
//reviver,可選,轉換器, 如果傳入該參數(函數),可以用來修改解析生成的原始值,調用時機在 parse 函數返回之前。

示例:

JSON.parse('{}');              // {}
JSON.parse('true');            // true
JSON.parse('"foo"');           // "foo"
JSON.parse('[1, 5, "false"]'); // [1, 5, "false"]
JSON.parse('null');            // null

JSON.stringify()方法將一個 JavaScript 對象或值轉換為 JSON 字符串,如果指定瞭一個 replacer 函數,則可以選擇性地替換值,或者指定的 replacer 是數組,則可選擇性地僅包含數組指定的屬性。

語法:

JSON.stringify(value[, replacer [, space]])
//value:將要序列化成 一個 JSON 字符串的值。
//replacer,可選,如果該參數是一個函數,則在序列化過程中,被序列化的值的每個屬性都會經過該函數的轉換和處理;如果該參數是一個數組,則隻有包含在這個數組中的屬性名才會被序列化到最終的 JSON 字符串中;如果該參數為 null 或者未提供,則對象所有的屬性都會被序列化。
//space,可選,指定縮進用的空白字符串,用於美化輸出(pretty-print);如果參數是個數字,它代表有多少的空格;上限為10。該值若小於1,則意味著沒有空格;如果該參數為字符串(當字符串長度超過10個字母,取其前10個字母),該字符串將被作為空格;如果該參數沒有提供(或者為 null),將沒有空格。

示例:

JSON.stringify({});                        // '{}'
JSON.stringify(true);                      // 'true'
JSON.stringify("foo");                     // '"foo"'
JSON.stringify([1, "false", false]);       // '[1,"false",false]'
JSON.stringify({ x: 5 });                  // '{"x":5}'

深拷貝示例代碼:

let people = { //定義一個People對象
    name: "張三",
    age: 3,
    address: "中國"
}
//對people進行深拷貝
let newPeople = JSON.parse(JSON.stringify(people));
console.log("原對象:", people);
console.log("新對象:", newPeople);
// 原對象: { name: '張三', age: 3, address: '中國' }
// 新對象: { name: '張三', age: 3, address: '中國' }
//修改新對象中的adress屬性
newPeople.address = "俄羅斯";
console.log("原對象:", people);
console.log("新對象:", newPeople);
// 原對象: { name: '張三', age: 3, address: '中國' }
// 新對象: { name: '張三', age: 3, address: '俄羅斯' }

用JSON.stringify將對象轉成JSON字符串,再用JSON.parse()把字符串解析成對象,這樣新的對象產生瞭,實現深拷貝。這種方法雖然可以實現數組或對象深拷貝,但不能處理函數。

let arr1 = [1, 2, { name: "橘貓吃不胖" }, function () { }];
//進行深拷貝
let arr2 = JSON.parse(JSON.stringify(arr1));
console.log("原數組:", arr1);
console.log("新數組:", arr2);
// 原數組: [ 1, 2, { name: '橘貓吃不胖' }, [Function (anonymous)] ]
// 新數組: [ 1, 2, { name: '橘貓吃不胖' }, null ]

由上面例子可以看出,函數並沒有被拷貝在arr2中。這是因為 JSON.stringify() 方法是將一個JavaScript值(對象或者數組)轉換為一個 JSON字符串,不能接受函數。

4.2 函數庫lodash

Lodash是一個JavaScript庫,提供瞭多個實用程序功能,而Lodash庫中最常用的功能之一是cloneDeep()方法。此方法有助於深度克隆對象,還可以克隆JSON.stringify()方法的局限性,即不可序列化的屬性。

示例代碼:

const lodash = require("lodash");
let people = {
    name: "張三",
    age: 3,
    address: "中國"
}
//對people進行深拷貝
let newPeople = lodash.cloneDeep(people);
console.log("原對象:", people);
console.log("新對象:", newPeople);
// 原對象: { name: '張三', age: 3, address: '中國' }
// 新對象: { name: '張三', age: 3, address: '中國' }
//修改新對象中的adress屬性
newPeople.address = "俄羅斯";
newPeople.name = "橘貓吃不胖";
console.log("原對象:", people);
console.log("新對象:", newPeople);
// 原對象: { name: '張三', age: 3, address: '中國' }
// 新對象: { name: '橘貓吃不胖', age: 3, address: '俄羅斯' }

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!    

推薦閱讀: