js淺拷貝和深拷貝


淺度拷貝:復制一層對象的屬性,並不包括對象里面的為引用類型的數據,當改變拷貝的對象里面的引用類型時,源對象也會改變。·

深度拷貝:重新開辟一個內存空間,需要遞歸拷貝對象里的引用,直到子屬性都為基本類型。兩個對象對應兩個不同的地址,修改一個對象的屬性,不會改變另一個對象的屬性。

1、javaScript的變量類型

(1)基本類型:
5種基本數據類型Undefined、Null、Boolean、Number 和 String,變量是直接按值存放的,存放在棧內存中的簡單數據段,可以直接訪問。

(2)引用類型:
存放在堆內存中的對象,變量保存的是一個指針,這個指針指向另一個位置。當需要訪問引用類型(如對象,數組等)的值時,首先從棧中獲得該對象的地址指針,然后再從堆內存中取得所需的數據。

JavaScript存儲對象都是存地址的,所以淺拷貝會導致 obj1 和obj2 指向同一塊內存地址。改變了其中一方的內容,都是在原來的內存上做修改會導致拷貝對象和源對象都發生改變,而深拷貝是開辟一塊新的內存地址,將原對象的各個屬性逐個復制進去。對拷貝對象和源對象各自的操作互不影響。

淺拷貝和深拷貝的區分

首先深復制和淺復制只針對像 Object, Array 這樣的復雜對象的。簡單來說,淺復制只復制一層對象的屬性,而深復制則遞歸復制了所有層級。

拋開jQuery,上代碼例子。下面是一個簡單的淺復制實現:

var obj = { a:1, arr: [2,3] }; var shallowObj = shallowCopy(obj); function shallowCopy(src) { var dst = {}; for (var prop in src) { if (src.hasOwnProperty(prop)) { dst[prop] = src[prop]; } } return dst; } 

因為淺復制只會將對象的各個屬性進行依次復制,並不會進行遞歸復制,而 JavaScript 存儲對象都是存地址的,所以淺復制會導致 obj.arr 和 shallowObj.arr 指向同一塊內存地址,大概的示意圖如下。

v2-39761dfd012733879e0d100ec260a5d7_hd

導致的結果就是:

shallowObj.arr[1] = 5; obj.arr[1] // = 5 

而深復制則不同,它不僅將原對象的各個屬性逐個復制出去,而且將原對象各個屬性所包含的對象也依次采用深復制的方法遞歸復制到新對象上。這就不會存在上面 obj 和 shallowObj 的 arr 屬性指向同一個對象的問題。

var obj = { a:1, arr: [1,2] }; var obj2 = deepCopy(obj); 

結果如下面的示意圖所示:

6604224933c95787764d941432a1f968_hd

需要注意的是,如果對象比較大,層級也比較多,深復制會帶來性能上的問題。在遇到需要采用深復制的場景時,可以考慮有沒有其他替代的方案。在實際的應用場景中,也是淺復制更為常用。

2、淺拷貝的實現

2.1、簡單的引用復制

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

2.2、Object.assign()

Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然后返回目標對象。

var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [ 1, 2, 3 ]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f);     // true

3、深拷貝的實現

3.1、Array的slice和concat方法

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

  • 如果該元素是個對象引用 (不是實際的對象),slice 會拷貝這個對象引用到新的數組里。兩個對象引用都引用了同一個對象。如果被引用的對象發生改變,則新的和原來的數組中的這個元素也會發生改變。
  • 對於字符串、數字及布爾值來說(不是 String、Number 或者 Boolean 對象),slice 會拷貝這些值到新的數組里。在別的數組里修改這些字符串或數字或是布爾值,將不會影響另一個數組。

如果向兩個數組任一中添加了新元素,則另一個不會受到影響。例子如下:

var array = [1,2,3]; 
var array_shallow = array; 
var array_concat = array.concat(); 
var array_slice = array.slice(0); 
console.log(array === array_shallow); //true 
console.log(array === array_slice); //false,“看起來”像深拷貝
console.log(array === array_concat); //false,“看起來”像深拷貝

可以看出,concat和slice返回的不同的數組實例,這與直接的引用復制是不同的。而從另一個例子可以看出Array的concat和slice並不是真正的深復制,數組中的對象元素(Object,Array等)只是復制了引用。如下:

var array = [1, [1,2,3], {name:"array"}]; 
var array_concat = array.concat();
var array_slice = array.slice(0);
array_concat[1][0] = 5;  //改變array_concat中數組元素的值 
console.log(array[1]); //[5,2,3] 
console.log(array_slice[1]); //[5,2,3] 
array_slice[2].name = "array_slice"; //改變array_slice中對象元素的值 
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice

3.2、JSON對象的parse和stringify

JSON對象是ES5中引入的新的類型(支持的瀏覽器為IE8+),JSON對象parse方法可以將JSON字符串反序列化成JS對象,stringify方法可以將JS對象序列化成JSON字符串,借助這兩個方法,也可以實現對象的深拷貝。

//例1
var source = { name:"source", child:{ name:"child" } } 
var target = JSON.parse(JSON.stringify(source));
target.name = "target";  //改變target的name屬性
console.log(source.name); //source 
console.log(target.name); //target
target.child.name = "target child"; //改變target的child 
console.log(source.child.name); //child 
console.log(target.child.name); //target child
//例2
var source = { name:function(){console.log(1);}, child:{ name:"child" } } 
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
//例3
var source = { name:function(){console.log(1);}, child:new RegExp("e") }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}

這種方法使用較為簡單,可以滿足基本的深拷貝需求,而且能夠處理JSON格式能表示的所有數據類型,但是對於正則表達式類型、函數類型等無法進行深拷貝(而且會直接丟失相應的值)。還有一點不好的地方是它會拋棄對象的constructor。也就是深拷貝之后,不管這個對象原來的構造函數是什么,在深拷貝之后都會變成Object。同時如果對象中存在循環引用的情況也無法正確處理。

4、jQuery.extend()方法源碼實現

jQuery的源碼 - src/core.js #L121源碼及分析如下:

jQuery.extend = jQuery.fn.extend = function() { //給jQuery對象和jQuery原型對象都添加了extend擴展方法
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1,
  length = arguments.length,
  deep = false;
  //以上其中的變量:options是一個緩存變量,用來緩存arguments[i],name是用來接收將要被擴展對象的key,src改變之前target對象上每個key對應的value。
  //copy傳入對象上每個key對應的value,copyIsArray判定copy是否為一個數組,clone深拷貝中用來臨時存對象或數組的src。

  // 處理深拷貝的情況
  if (typeof target === "boolean") {
    deep = target;
    target = arguments[1] || {};
    //跳過布爾值和目標 
    i++;
  }

  // 控制當target不是object或者function的情況
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 當參數列表長度等於i的時候,擴展jQuery對象自身。
  if (length === i) {
    target = this; --i;
  }
  for (; i < length; i++) {
    if ((options = arguments[i]) != null) {
      // 擴展基礎對象
      for (name in options) {
        src = target[name];	
        copy = options[name];

        // 防止永無止境的循環,這里舉個例子,如var i = {};i.a = i;$.extend(true,{},i);如果沒有這個判斷變成死循環了
        if (target === copy) {
          continue;
        }
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是數組的話就讓clone副本等於src否則等於空數組。
          } else {
            clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是對象的話就讓clone副本等於src否則等於空數組。
          }
          // 遞歸拷貝
          target[name] = jQuery.extend(deep, clone, copy);
        } else if (copy !== undefined) {
          target[name] = copy; // 若原對象存在name屬性,則直接覆蓋掉;若不存在,則創建新的屬性。
        }
      }
    }
  }
  // 返回修改的對象
  return target;
};

jQuery的extend方法使用基本的遞歸思路實現了淺拷貝和深拷貝,但是這個方法也無法處理源對象內部循環引用,例如:

var a = {"name":"aaa"};
var b = {"name":"bbb"};
a.child = b;
b.parent = a;
$.extend(true,{},a);//直接報了棧溢出。Uncaught RangeError: Maximum call stack size exceeded

5、自己動手實現一個拷貝方法

var $ = (function () {
    'use strict';

    var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');

	function type () {
	   return Object.prototype.toString.call(this).slice(8, -1);
	}

	for (var i = types.length; i--;) {
	    $['is' + types[i]] = (function (self) {
	        return function (elem) {
	           return type.call(elem) === self;
	        };
	    })(types[i]);
	}

    return $;
})();//類型判斷

function copy (obj,deep) { 
    if (obj === null || (typeof obj !== "object" && !$.isFunction(obj))) { 
        return obj; 
    } 

    if ($.isFunction(obj)) {
    	return new Function("return " + obj.toString())();
    }
    else {
        var name, target = $.isArray(obj) ? [] : {}, value; 

        for (name in obj) { 
            value = obj[name]; 
            if (value === obj) {
            	continue;
            }

            if (deep && ($.isArray(value) || $.isObject(value))) {
            	target[name] = copy(value,deep);
            }
            else {
            	target[name] = value;
            } 
        } 
        return target;
    }         
}

原文:javascript中的深拷貝和淺拷貝區分以及實現 ,未經許可,禁止轉載。
來源:前端開發博客 (http://caibaojian.com/javascript-object-clone.html)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM