爲何寫拷貝這篇文章?同事有一天提到了拷貝,他說賦值就是一種淺拷貝方式,另外一個同事說賦值和淺拷貝並不相同。
我也有些疑惑,因而我去MDN搜一下拷貝相關內容,發現並無關於拷貝的實質概念,沒有辦法只能經過實踐了,同時去看一些前輩們的文章總結了這篇關於拷貝的內容,本文也屬於公衆號【程序員成長指北】學習路線中【JS必知必會】內容。javascript
棧內存
中(不包含閉包
中的變量)
堆內存
中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址(引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中得到實體。
注意:前端
閉包
中的變量並不保存在棧內存中,而是保存在堆內存中。這一點比較好想,若是閉包
中的變量保存在了棧內存
中,隨着外層中的函數從調用棧中銷燬,變量確定也會被銷燬,可是若是保存在了堆內存中,內存函數仍能訪問外層已銷燬函數中的變量。看一段對應代碼理解下:function A() { let a = 'koala' function B() { console.log(a) } return B }
看一段代碼java
let a ='koala'; let b = a; b='程序員成長指北'; console.log(a); // koala
基本數據類型複製配圖:程序員
結論:在棧內存中的數據發生數據變化的時候,系統會自動爲新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。api
看一段代碼數組
let a = {x:'kaola', y:'kaola1'} let b = a; b.x = '程序員成長指北'; console.log(a.x); // 程序員成長指北
引用數據類型複製配圖:閉包
結論:引用類型的複製,一樣爲新的變量b分配一個新的值,報錯在棧內存中,不一樣的是這個變量對應的具體值不在棧中,棧中只是一個地址指針。兩個變量地址指針相同,指向堆內存中的對象,所以b.x發生改變的時候,a.x也發生了改變。koa
不知道的api我通常比較喜歡看MDN,淺拷貝的概念MDN官方並無給出明肯定義,可是搜到了一個函數Array.prototype.slice,官方說它能夠實現原數組的淺拷貝。
對於官方給的結論,咱們經過兩段代碼驗證一下,並總結出淺拷貝的定義。函數
var a = [ 1, 3, 5, { x: 1 } ]; var b = Array.prototype.slice.call(a); b[0] = 2; console.log(a); // [ 1, 3, 5, { x: 1 } ]; console.log(b); // [ 2, 3, 5, { x: 1 } ];
從輸出結果能夠看出,淺拷貝後,數組a[0]並不會隨着b[0]改變而改變,說明a和b在棧內存中引用地址並不相同。學習
var a = [ 1, 3, 5, { x: 1 } ]; var b = Array.prototype.slice.call(a); b[3].x = 2; console.log(a); // [ 1, 3, 5, { x: 2 } ]; console.log(b); // [ 1, 3, 5, { x: 2 } ];
從輸出結果能夠看出,淺拷貝後,數組中對象的屬性會根據修改而改變,說明淺拷貝的時候拷貝的已存在對象的對象的屬性引用。
經過這個官方的slice
淺拷貝函數分析淺拷貝定義
:
新的對象複製已有對象中非對象屬性的值和對象屬性的引用。若是這種說法不理解換一種一個新的對象直接拷貝已存在的對象的對象屬性的引用,即淺拷貝。
語法:Object.assign(target, ...sources)
ES6中拷貝對象的方法,接受的第一個參數是拷貝的目標target
,剩下的參數是拷貝的源對象sources
(能夠是多個)
let target = {}; let source = {a:'koala',b:{name:'程序員成長指北'}}; Object.assign(target ,source); console.log(target); // { a: 'koala', b: { name: '程序員成長指北' } } source.a = 'smallKoala'; source.b.name = '程序員成長指北哦' console.log(source); // { a: 'smallKoala', b: { name: '程序員成長指北哦' } } console.log(target); // { a: 'koala', b: { name: '程序員成長指北哦' } }
從打印結果能夠看出,Object.assign
是一個淺拷貝,它只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
undefined
和null
沒法轉成對象,它們不能做爲Object.assign
參數,可是能夠做爲源對象Object.assign(undefined) // 報錯 Object.assign(null) // 報錯 let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true
Symbol
值的屬性,能夠被Object.assign拷貝。這個函數在淺拷貝概念定義的時候已經進行了分析,看上文。
var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
參數:將數組和/或值鏈接成新數組
let array = [{a: 1}, {b: 2}]; let array1 = [{c: 3},{d: 4}]; let array2=array.concat(array1); array1[0].c=123; console.log(array2);// [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ] console.log(array1);// [ { c: 123 }, { d: 4 } ]
Array.prototype.concat也是一個淺拷貝,只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
var cloneObj = { ...obj };
let obj = {a:1,b:{c:1}} let obj2 = {...obj}; obj.a=2; console.log(obj); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2; console.log(obj); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
擴展運算符也是淺拷貝,對於值是對象的屬性沒法徹底拷貝成2個不一樣對象,可是若是屬性都是基本類型的值的話,使用擴展運算符也是優點方便的地方。
補充說明:以上4中淺拷貝方式都不會改變原數組,只會返回一個淺拷貝了原數組中的元素的一個新數組。
實現原理:新的對象複製已有對象中非對象屬性的值和對象屬性的引用
,也就是說對象屬性並不複製到內存。
function cloneShallow(source) { var target = {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } return target; }
for in
for...in語句以任意順序遍歷一個對象自有的、繼承的、可枚舉的
、非Symbol的屬性。對於每一個不一樣的屬性,語句都會被執行。
hasOwnProperty
語法:obj.hasOwnProperty(prop)
prop是要檢測的屬性字符串
名稱或者Symbol
該函數返回值爲布爾值,全部繼承了 Object 的對象都會繼承到 hasOwnProperty 方法,和 in 運算符不一樣,該函數會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。
說了賦值操做和淺拷貝操做,你們是否是已經能想到什麼是深拷貝了,下面直接說深拷貝的定義。
深拷貝會另外拷貝一份一個如出一轍的對象,從堆內存中開闢一個新的區域存放新對象,新對象跟原對象不共享內存,修改新對象不會改到原對象。
JSON.stringify()是前端開發過程當中比較經常使用的深拷貝方式。原理是把一個對象序列化成爲一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()反序列化將JSON字符串變成一個新的對象
let arr = [1, 3, { username: ' koala' }]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = 'smallKoala'; console.log(arr4);// [ 1, 3, { username: 'smallKoala' } ] console.log(arr);// [ 1, 3, { username: ' koala' } ]
實現了深拷貝,當改變數組中對象的值時候,原數組中的內容並無發生改變。JSON.stringify()雖然能夠實現深拷貝,可是還有一些弊端好比不能處理函數等。
深拷貝,主要用到的思想是遞歸,遍歷對象、數組直到裏邊都是基本數據類型,而後再去複製,就是深度拷貝。
實現代碼:
//定義檢測數據類型的功能函數 function isObject(obj) { return typeof obj === 'object' && obj != null; } function cloneDeep(source) { if (!isObject(source)) return source; // 非對象返回自身 var target = Array.isArray(source) ? [] : {}; for(var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key])) { target[key] = cloneDeep(source[key]); // 注意這裏 } else { target[key] = source[key]; } } } return target; }
該簡單深拷貝未考慮內容:
遇到循環引用,會陷入一個循環的遞歸過程,從而致使爆棧
// RangeError: Maximum call stack size exceeded
小夥伴們有沒有什麼好辦法呢,能夠寫下代碼在評論區一塊兒討論哦!
該函數庫也有提供_.cloneDeep用來作 Deep Copy(lodash是一個不錯的第三方開源庫,有好多不錯的函數,也能夠看具體的實現源碼)
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
用一張圖總結
今天就分享這麼多,若是對分享的內容感興趣,能夠關注公衆號「程序員成長指北」,或者加入技術交流羣,你們一塊兒討論。
進階技術路線
加入咱們一塊兒學習吧!