深拷貝與淺拷貝問題

JavaScript的變量類型正則表達式

五種基本變量類型Null, Undefined, Boolean, Number, String,變量都是按值存放的,存放在棧內存中的簡單數據段,能夠直接訪問。對於引用類型,是存放在棧中的對象,變量保存的是一個指針,這個指針指向另外一個位置。當須要訪問引用類型(如對象,數組等)的值時,首先從棧中獲取該對象的地址指針,而後再從堆內存中取得所需的數據。JavaScript存儲對象都是存地址的,因此淺拷貝會致使obj1和obj2只想同一塊內存地址。改變了其中一方的內容,都是在原來的內存上作修改會致使拷貝對象和原對象都發生改變,而深拷貝是開闢一塊新的內存地址,將原對象的各個屬性逐個複製進去。對拷貝對象和原對象各自的操做各不影響。數組

eg1:緩存

//數組拷貝
var arr1 = [1,2,3];
var arr2 = arr1;
arr1[0] = "e";
console.log(arr1);//"e,2,3"
console.log(arr2);//"e,2,3"

淺拷貝的實現函數

  1. 引用複製
function shallowClone(copyObj) {
    var obj = {};
    for(var i in copyObj) {
        obj[i] = copyObj[i];
    }
}
var x = {
    a:1,
    b:{d:{e:5}},
    c:[1,2,3]
};
var y = shallowClone(x);
console.log(y.b.d===x.b.d);//true;

  2. Object.assign()oop

  Object.assign方法用於對象的合併,並將源對象(source)的全部可枚舉屬性,複製到目標對象(target)。當目標對象和源對象有同名屬性時,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。測試

const target = {a: 1,b: 1};
const source1 = {b: 2};
const source2 = {c: 3};
Object.assign(target, source1, source2);
console.log(target) //{a: 1, b: 2, c: 3}

Object.assign()拷貝屬性是有限制的,只拷貝原對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性(enumerable: false)this

Object.assign({b: 'c'},
    Object.defineProperty({}, 'invisible', {
        enumerable: false,
        value: 'hello'
    })
)
//{b: 'c'}

Object.assign方法實行的是淺拷貝,而不是深拷貝。也就是說,若是源對象某個屬性的值是對象,那麼目標對象拷貝獲得的是這個對象的引用。spa

const obj1 = {a: {b: 5}};
const obj2 = Object.assign({}, x);
obj1.a.b=2;
console.log(obj2.a.b) //2

上面代碼中,源對象obj1a屬性的值是一個對象,Object.assign拷貝獲得的是這個對象的引用。這個對象的任何變化,都會反映到目標對象上面。prototype

//採用這種方法能夠克隆它繼承的值
function clone(origin){
    let originProto = Object.getPrototypeOf(origin);
    return Object.assign(Object.create(originProto), origin);
}

 

深拷貝的實現指針

   1.Array的slice和concat方法

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

  •  若是該元素是個對象引用,slice會拷貝這個對象引用到新的數組中。兩個對象引用都引用了同一個對象。若是被引用的對象發生改變,則新的和原來的數組中的這個元素也會發生改變。
  • 對於字符串、數字、布爾值來講,slice會拷貝這些值到新的數組中,在別的數組裏修改這些字符串或數字或是布爾值,將不會影響另外一個數組。
var arr = [1, 2, 3];
var arr_shallow = arr;
var arr_concat = arr.concat();
var arr_slice = arr.slice(0);
console.log(arr===arr_shallow);//true;
console.log(arr==arr_slice);//false;
console.log(arr===arr_concat);//false;

能夠看到,concat和slice返回不一樣的數組實例,這與直接引用複製是不一樣的。而從另外一個例子能夠看出Array的concat和slice並非真正的深拷貝,數組中的對象元素(Object, Array)只是複製了引用。以下:

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

    1.1 遞歸實現對象的深拷貝

function deepCopy(obj) {
    var target = Array.isArray(obj) ? [] : {};
    for(var key in obj){
        if(typeof(obj[key]) == 'object' || Array.isArray(obj[key]))
            target[key] = deepCopy(obj[key]);
        else
            target[key] = obj[key];
    }
    return target;
}

  2. JSON對象的parse和stringify

JSON對象parse方法能夠將JSON字符串反序列化成JS對象,stringify方法能夠將JS對象序列化成JSON字符串,藉助這兩個方法,能夠實現對象的深拷貝

 
 
//eg1
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
 
 
 
//eg2
var source = {name: function(){console.log(1);}, child:{name: "child"}};
var target = JSON.parse(JSON.stringify(source));
console.log(target.name);//undefined;
 
 
 
//eg3
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。同時若是對象中存在循環引用的狀況也沒法正確處理。

  3. jQuery.extend()方法源碼實現

/** 
*$.extend([deep], clone, copy)
*[options]用來緩存arguments[i]
*[name]用來接收將要被擴展對象的key
*[src]表示改變以前,target對象上每一個key所對應的value,即target[name]
*[copy]表示傳入對象上每一個key所對應的value, 即options[name]
*[copyIsArray]斷定copy是否爲一個數組
*[clone]深拷貝中用來臨時存儲對象或數組的src
*/
jQuery.extend = jQuery.fn.extend = function( ) {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {};
        i=1,
        length = arguments.length,
        deep = false;
    //Handle a deep copy situation
    if(typeof target === "boolean") {
        deep = target;
        //Skip the boolean and target
        target = arguments[i] || {};
        i++;
    }
    //Handle case when target is a string or something(possible in deep copy)
    if(typeof target!="object" && !isFunction(target)) {
        target = {};
    }
    //Extend jQuery itself if only one argument is passed
    if(i === length) {
        target = this;
        i--;
    }
    for(; i< length; i++) {

        //Only deal with non-null/undefined values
        if((options = arguments[i])!=null) {

            //Extend the base object
            for(name in options) {
                src = target[name];
                copy = options[name];

                //Prevent never-ending loop such as var i = {}; i.a = i; $.extend(true, {}, i);
                if(target === copy) {
                    continue;
                }

                //Recurse if we're merging plain objects or arrays
                if(deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
                    if(copyIsArray) {
                        copyIsArray = false;
                        clone = src && Array.isArray(src) ? src : [];
                    else {
                        clone = src && jQuery.isPlainObject(src) ? src : {};
                    }
                    //Never move original objects, clone them
                    target[name] = jQuery.extend(deep, clone, copy);
                //Don't bring in undefined values
                } else if(copy !== undefined) {
                     target[name] = copy;//if find name, cover it,or make it;
                }
            }
        }
    }
    //Return the modified object
    return target;
};

這個方法到目前仍是不能處理原對象內部的循環引用,

var a = {name: "knight"},
    b = {name: "knightboy"};
a.child = b;
b.parent = a;
$.extend(true, {}, a);
//Uncaught RangeError: Maximum call stack size exceeded

  4. 自定義實現深拷貝

    // 偏函數
   //判斷類型 boolean
var $ = (function() { var $={}; 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(obj) { return type.call(obj) === self; }; })(types[i]); } return $; })(); function copy(obj, key, deep) { var index = 0; if (obj === null || typeof obj !== "object") { return obj; } var name, value, target = $.isArray(obj) ? [] : {}; for (name in obj) { value = obj[name]; if (value === obj) { continue; } if(index===0) value.copykey = 1; if (deep && ($.isArray(value) || $.isObject(value))) { if (!value.copykey && key!==name) { target[name] = copy(value, name, deep); delete target[name].copykey; delete value.copykey; } else { target[name] = value; delete target[name].copykey; delete value.copykey; } } else { target[name] = value; } index=1; } return target; } //嵌套 const obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2; }, a: Symbol("foo") }; const obj2 = copy(obj1,{},true); obj2.x.m = 2; //原型鏈 function SupType(){ this.SupProperty = "knight"; } function SubType(){ this.SubProperty = 19; } SupType.prototype.getSupValue = function(){ return this.SupProperty; }; SubType.prototype = new SupType(); var instance = new SubType(); let origin = instance.getSupValue(); //循環引用test1 var a = {name: "knight"}, b = {name: "knightboy"}; a.child = b; b.parent = a; //循環引用test2 function P(obj){ } P.prototype.test = function(){console.log(1)}; var p =new RegExp('/[.]/'); var obj3,obj4; //var obj5=new P(); obj3={a:1,b:2,c:3,d:{a:1,b:2,c:3,d:{a:1,b:2,c:3,d:{a:1,b:2,c:3,d:Date.now(),e:p}}}}; obj4={a:1,b:2,c:3,d:Date.now(),e:p}; obj3.a = obj4; obj4.a = obj3; obj3.f = obj4; obj4.f = obj3; var res = copy(obj3,{},true); console.log("源對象(copy):\n",obj3); console.log("目標對象(clone):\n",res); res.b=5; console.log(res.b=obj3.b); console.log("二次環引用測試():\n",obj4);

 

//數據類型判斷還有一種更加簡潔的方法:
var Type = {};
for(var i=0,type;type=['String','Array','Number','Object','Boolean','Undefined','Null','Symbol','Date','RegExp','Function'][i++];){
    (function(type){
       Type['is'+ type] = function(obj){
          return Object.prototype.toString.call(obj) === '[object ' + type + ']';
       }
    })(type);
}
Type.isArray([]);//true
相關文章
相關標籤/搜索