[筆記] Javascript 深拷貝與淺拷貝


這就得先來談談 Javascript 的基本型別(Primitive Type) vs 物件(Object)

Data Type分別為:
  • Primitive Type: Number, String, Boolean, Null, Undefined
  • Object: Object, Array, Function, Date, Regx
基本型別和物件的傳值方式不同:
  • 基本型別是 by Value
  • 物件是by reference

如果是 Primitive Type
  1. var copyMe = 1234;
  2.  
  3. function copy(o) {
  4. var copyObj = o;
  5.  
  6. // 直接給值
  7. copyObj = 55;
  8.  
  9. console.log(o); //1234 不變
  10. console.log(copyObj);//55
  11.  
  12. }
  13. copy(copyMe);
如果是 By Value 的方式,不會改變原本的。

假如有個需要複製的物件(接下來都用這個來舉例)
  1. var copyThis = {a:11, b:21, c:33};
以下舉例
  1. function copy1(o) {
  2. var copyObj = o;
  3.  
  4. // 直接給值
  5. copyObj = 55;
  6.  
  7. console.log(o); //{a:11, b:21, c:33}
  8. console.log(copyObj);//55
  9. //-----------------------------------------------
  10. copyObj2 = o;
  11. copyObj2.a = 60;
  12.  
  13. console.log(o); //{a:60, b:21, c:33} 被改變
  14. console.log(copyObj2);//{a:60, b:21, c:33}
  15.  
  16. }
  17. copy1(copyThis);
當直接給值55的時候,又重新定義了 copyObj,所以不影響;
但是在下半段,修改 copyObj2.a 連動到 o.a,因為他們指向同一個節點位置,所以原本的也被改變了。

So...
  • 淺拷貝:就是物件指向某個指標(Node),但還是共用同一塊記憶體。
  • 深拷貝:就是另外建立新的物件,有同樣的內容,且不共用記憶體。

但還是有些例外
  1. function copy2(o) {
  2. var copyObj = o;
  3.  
  4. // 直接給值
  5. o = {a:33, b:55, c:77};
  6.  
  7. console.log(o); //{a:33, b:55, c:77} 被改變
  8. console.log(copyObj);//{a:11, b:21, c:33} 不變
  9. }
  10. copy2(copyThis);
object literal 的方式指定物件的值,那麼就會是 by value

咦!什麼是 object literal?
請參考 [筆記] 談談JavaScript中的物件建立(Object) - Part 2 | 利用大括號{}建立物件

如果不更動到原本的呢?
  1. function copy3(o) {
  2. var copyObj = {};
  3. copyObj = {
  4. a:o.a,
  5. b:o.b,
  6. c:o.c
  7. };
  8. copyObj.a = 14;
  9.  
  10. console.log(o); // {a:11, b:21, c:33}
  11. console.log(copyObj); //{a:14, b:21, c:33}
  12.  
  13. }
  14. copy3(copyThis);
雖然這樣可以避免,但這樣一個個列出太麻煩,且這樣不是deep copy

那麼,如果使用Object.create()??
  1. function copy6(o) {
  2. var copyObj = Object.create(o);
  3. copyObj.a = 77;
  4. copyObj.b = 66;
  5. console.log(o); // {a:11, b:21, c:33}
  6. console.log(copyObj.a); // {a: 77, b: 66, c: 33}
  7.  
  8. }
  9. copy6(copyThis);
在這邊請注意,可能發生以下狀況:當Object.create()遇上 Array
  1. var copyArr = [{a: 11, b: 21, c: 33}];
  2. function copy7(o) {
  3. var copyObj = Object.create(o);
  4. copyObj[0].a = 88;
  5.  
  6. console.log(o); // {a: 88, b: 21, c: 33} //被改變了...
  7. console.log(copyObj); // a: 88, b: 21, c: 33}
  8.  
  9. }
  10. copy7(copyArr);
為什麼呢?Javascript Arrays created with Object.create - not real Arrays?
總結就是,Object.create()而言,是建立一個Array屬性的「物件」,這又回到 by reference 的問題了。

恩⋯⋯那如果反過來,Array.form()Object.create()合併使用?
  1. var copyArr7_1 = [11, 21, 33];
  2. function copy7_1(o) {
  3. var copyObj = Array.from(Object.create(o));
  4. copyObj[0] = 88;
  5.  
  6. console.log(o); // [11, 21, 33]
  7. console.log(copyObj); // [88, 21, 33]
  8.  
  9. }
  10. copy7_1(copyArr7_1);
此時發現,原本的不會被改變,但是如果遇到以下狀況⋯⋯
  1. var copyArr7_2 = [{a:11}, {b:21}, {c:33}];
  2. function copy7_2(o) {
  3. var copyObj = Array.from(Object.create(o));
  4. copyObj[0].a = 88;
  5.  
  6. console.log(o); // {a: 88, b: 21, c: 33} //被改變了...
  7. console.log(copyObj); // {a: 88, b: 21, c: 33}
  8.  
  9. }
  10. copy7_2(copyArr7_2);
這時就得探討Array.form()

解法:先JSON.stringify轉成 JSON 格式,再JSON.parse解開。
  1. function copy4(o) {
  2. var copyObj = JSON.parse(JSON.stringify(o));
  3.  
  4. copyObj.a = 14;
  5.  
  6. console.log(o); // {a:11, b:21, c:33}
  7. console.log(copyObj); // {a: 14, b: 21, c: 33}
  8.  
  9. }
  10.  
  11. copy4(copyThis);
解法:使用 ES6 新的函式 Object.assign()
  1. function copy5(o) {
  2. var copyObj = Object.assign({},o);
  3.  
  4. copyObj.a = 14;
  5.  
  6. console.log(o); // {a:11, b:21, c:33}
  7. console.log(copyObj); // {a: 14, b: 21, c: 33}
  8.  
  9. }
  10.  
  11. copy5(copyThis);
Object.assign({},o){}意思是另建立一個空物件,再將o屬性質複製過去,因此此方法只針對一層的物件有用。可以用在一些簡單的需求。

當然,運用 library 也可以快速達到 deep copy 效果:
  • lodash 的 cloneDeep()
  • jQuery 的 extend()

最後,放上小小的實作⋯⋯
  1. //訂單結帳
  2. var shoppingCar = [
  3. {usr_id:'AB123',ord_id:'NA20171212',items:[
  4. {item:'Pants', it_price:'23'},
  5. {item:'Shirt', it_price:'21'},
  6. {item:'Top', it_price:'15'},
  7. function contactMethod () {
  8. return 'AAAAA';
  9. }]
  10. },
  11. {name:'Aero',mobile:'+8869000000',add:'Taipei, Taiwan'}
  12. ];
  13.  
  14. function checkout(order, rate) {
  15. var output = {};
  16.  
  17. for (var key in order) {
  18. var o = order[key];
  19.  
  20. if (key === "it_price"){
  21. o = discount(rate, o);
  22. }
  23.  
  24. output[key] = o;
  25.  
  26. if (typeof o === 'function') {
  27. output[key] = new Function("return "+o.toString());
  28. }
  29.  
  30. if (typeof o === 'object') {
  31. output[key] = checkout(o, rate);
  32. }
  33. }
  34.  
  35. return output;
  36. }
  37.  
  38. function discount(rate, price) {
  39. var output = price * rate/100;
  40. return output;
  41. }
  42.  
  43. var myOrder = checkout(shoppingCar, 70);
  44. console.log(myOrder);
  45. console.log(shoppingCar);

參考資料
https://pjchender.blogspot.tw/2016/03/javascriptby-referenceby-value.html
https://www.codementor.io/avijitgupta/deep-copying-in-js-7x6q8vh5d
http://larry850806.github.io/2016/09/20/shallow-vs-deep-copy/
https://github.com/wengjq/Blog/issues/3