JavaScript中對象的複製、淺拷貝、深拷貝

以前在開發中有遇到拷貝的問題,我也看到剛入門同窗在用vue開發時所寫的賦值相關問題,在使用時,概念使得不清楚使賦值數據模糊不清。爲了幫助他人或者幫助本身更能深入記住,在查閱了相關資料以後在此記錄下本身的小小看法。在弄清楚拷貝以前咱們先弄清楚數據類型和堆棧的關係

數據類型與堆棧的關係

JS數據類型:JS 的數據類型有幾種?javascript

8種,Number、String、Boolean、Null、undefined、object、symbol、bigInt
Symbol

Symbol 本質上是一種惟一標識符,可用做對象的惟一屬性名,這樣其餘人就不會改寫或覆蓋你設置的屬性值html

Symbol 數據類型的特色是惟一性,即便是用同一個變量生成的值也不相等前端

let id1 = Symbol('id'); 
let id2 = Symbol('id'); 
console.log(id1 == id2); //false

Symbol 數據類型的另外一特色是隱藏性,for···in,object.keys() 不能訪問vue

let id = Symbol("id"); 
let obj = { [id]:'symbol' }; 
for(let option in obj){ 
    console.log(obj[option]); //空 
}

可是也有可以訪問的方法:Object.getOwnPropertySymbols,Object.getOwnPropertySymbols 方法會返回一個數組,成員是當前對象的全部用做屬性名的 Symbol 值java

JS數據類型:Object 中包含了哪幾種類型?jquery

其中包含了Object,Array,Date,Function,RegExp等

JS數據類型:JS的基本類型有哪些呢?git

基本類型(單類型):String、Number、boolean、null、undefined
BigInt

谷歌67版本中出現了一種bigInt,是指安全存儲、操做大整數。(可是不少人不把這個作爲一個類型)
BigInt數據類型的目的是比Number數據類型支持的範圍更大的整數值。在對大整數執行數學運算時,以任意精度表示整數的能力尤其重要。使用BigInt,整數溢出將再也不是問題。具體請看JavaScript 標準內置對象, JS最新基本數據類型:BigIntgithub

存儲方式

基本類型:基本類型值在內存中佔據固定大小,保存在`棧內存`中(不包含`閉包`中的變量)

引用類型:引用類型的值是對象,保存在`堆內存`中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址(引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中得到實體。

這裏用一張圖代表它們以前儲存的關係
20180511101107567.png數組

閉包與堆內存

閉包中的變量並不保存中棧內存中,而是保存在堆內存中。 這也就解釋了函數調用以後以後爲何閉包還能引用到函數內的變量。
咱們先來看什麼是閉包:安全

function A() {
  let a = '羊先生'
  function B() {
      console.log(a)
  }
  return B
}

函數 A 返回了一個函數 B,而且函數 B 中使用了函數 A 的變量,函數 B 就被稱爲閉包。
函數 A 調用後,函數 A 中的變量這時候是存儲在堆上的,因此函數B依舊能引用到函數A中的變量

賦值

基本數據類型複製

let a ='羊先生';
let b = a;
b='www.vipbic.com';
console.log(a); // 羊先生

結論:在棧內存中的數據發生數據變化的時候,系統會自動爲新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。

引用數據類型複製

let a = {x:'羊先生', y:'羊先生1'}
let b = a;
b.x = 'www.vipbic.com';
console.log(a.x); // www.vipbic.com

結論 引用類型的複製,一樣爲新的變量b分配一個新的值,保存在棧內存中,不一樣的是這個變量對應的具體值不在棧中,棧中只是一個地址指針。兩個變量地址指針相同,指向堆內存中的對象,所以b.x發生改變的時候,a.x也發生了改變

淺拷貝

Array的slice和concat方法

Array的slice和concat方法不修改原數組,只會返回一個淺複製了原數組中的元素的一個新數組。之因此把它放在淺拷貝里,是由於它看起來像是深拷貝。而實際上它是淺拷貝。原數組的元素會按照下述規則拷貝:

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 } ];

從輸出結果能夠看出,淺拷貝後,數組中對象的屬性會根據修改而改變,說明淺拷貝的時候拷貝的已存在對象的對象的屬性引用

...擴展運算符

語法: 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個不一樣對象

實現簡單的引用複製

function shallowClone(copyObj) {
  var obj = {};
  for ( var i in copyObj) {
    obj[i] = copyObj[i];
  }
  return obj;
}
var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f);     // true

Object.assign()

object.assign() 方法能夠把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,而後返回目標對象。

var obj1 = {
    title: '測試',
    subset: {
        name: '子對象',
        subpoll: {
            des: '子子對象'
        },
        sex: function() {
            console.log('sex')
        }
    },
    age: function() {
        console.log('age')
    }
}

//只從表面上來看,彷佛Object.assign()的目標對象是{},是一個新的對象(開闢了一塊新的內存空間),是深拷貝
var ojb2 = Object.assign({}, obj1)
ojb2.title = '1111' // obj1 不受影響 
ojb2.subset.name = '2222' // obj1 受影響 
ojb2.subset.subpoll.des = '3333' // obj1 受影響 

// 打印看結果
console.log('obj1:', obj1)
console.log('ojb2:', ojb2)

image.png
從打印結果能夠看出,Object.assign是一個淺拷貝,它只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
Object.assign注意事項
1.只拷貝源對象的自身屬性(不拷貝繼承屬性)
2.它不會拷貝對象不可枚舉的屬性
3.undefinednull沒法轉成對象,它們不能做爲Object.assign參數,可是能夠做爲源對象

Object.assign(undefined) // 報錯
Object.assign(null) // 報錯

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

4.屬性名爲Symbol值的屬性,能夠被Object.assign拷貝

深拷貝

JSON.parse(JSON.stringify())

JSON.stringify()是前端開發過程當中比較經常使用的深拷貝方式。原理是把一個對象序列化成爲一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()反序列化將JSON字符串變成一個新的對象

let arr = [1, 3, {
    username: '羊先生'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'www.vipbic.com'; 
console.log(arr4);// [ 1, 3, { username: 'www.vipbic.com' } ]
console.log(arr);// [ 1, 3, { username: ' 羊先生' } ]

實現了深拷貝,當改變數組中對象的值時候,原數組中的內容並無發生改變。JSON.stringify()雖然能夠實現深拷貝,可是還有一些弊端好比不能處理函數等。
JSON.stringify()實現深拷貝注意點
1.拷貝的對象的值中若是有函數,undefined,symbol則通過JSON.stringify()序列化後的JSON字符串中這個鍵值對會消失
2.沒法拷貝不可枚舉的屬性,沒法拷貝對象的原型鏈
3.拷貝Date引用類型會變成字符串
4.拷貝RegExp引用類型會變成空對象
5.對象中含有NaN、Infinity和-Infinity,則序列化的結果會變成null
6.沒法拷貝對象的循環應用(即obj[key] = obj)

總結:它會拋棄對象的constructor,深拷貝以後,無論這個對象原來的構造函數是什麼,在深拷貝以後都會變成Object;這種方法能正確處理的對象只有 Number, String, Boolean, Array, 扁平對象,也就是說,只有能夠轉成JSON格式的對象才能夠這樣用,像function沒辦法轉成JSON;

手動實現深拷貝

這是看github一個栗子,https://github.com/wengjq/Blo...

//類型判斷
(function($) {
    "use strict";
    var types = "Array,Object,String,Date,RegExp,Function,Boolean,Number,Null,Undefined".split(",");
    for (let i = types.length; i--; ) {
        $["is" + types[i]] = str => Object.prototype.toString.call(str).slice(8, -1) === types[i];
    }
    return $;
})(window.$ || (window.$ = {})); 

function copy(obj, deep = false, hash = new WeakMap()) {
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    if ($.isFunction(obj)) {
        return new Function("return " + obj.toString())();
    } else if (obj === null || typeof obj !== "object") {
        return obj;
    } else {
        var name,
        target = $.isArray(obj) ? [] : {},
        value;
        hash.set(obj, target);
        for (name in obj) {
            value = obj[name];
            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value, deep, hash);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                } else {
                    target[name] = value;
                }
            } else {
                target[name] = value;
            }
        }
      return target;
    }
}

//使用
var x = {};
var y = {};
x.i = y;
y.i = x;
var z = copy(x, true);
console.log(x, z);

var a = {};
a.a = a;
var b = copy(a, true);
console.log(a, b);

第三方深拷貝庫

lodash

該函數庫也有提供_.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
jQuery

jquery 提供一個$.extend能夠用來作深拷貝
語法:$.extend([deep], target, object1[, objectN] )
deep:表示是否深拷貝 默認爲false 爲true爲深拷貝,爲false,則爲淺拷貝
target: Object類型 目標對象,其餘對象的成員屬性將被附加到該對象上。
object1  objectN: 可選 Object類型 第一個以及第N個被合併的對象。

var $ = require('jquery');
var obj1 = {
   a: 1,
   b: {
     f: {
       g: 1
     }
   },
   c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);  // false

總結

www.vipbic.com.jpg

參考文章

JavaScript中的 Object 類型
javaScript中淺拷貝和深拷貝的實現
深拷貝與淺拷貝的實現

關於我

https://www.vipbic.com.png

相關文章
相關標籤/搜索