EcmaScript對象克隆之謎

先談談深拷貝

如何在js中得到一個克隆對象,能夠說是喜聞樂見的話題了。相信你們都瞭解引用類型與基本類型,也都知道有種叫作深拷貝的東西,傳說深拷貝能夠得到一個克隆對象!那麼像我這樣的萌新天然就去學習了一波,咱們能找到的代碼基本都是這樣的:數組

低配版深拷貝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var deepClone = function(currobj){
    if(typeof currobj !== 'object'){
        return currobj;
    }
    if(currobj instanceof Array){
        var newobj = [];
    }else{
        var newobj = {}
    }
    for(var key in currobj){
        if(typeof currobj[key] !== 'object'){
            newobj[key] = currobj[key];
        }else{
            newobj[key] = deepClone(currobj[key])    
        }
    }
    return newobj
}

嘖嘖真是很精巧啊!對於Array和普通Object都作了區分。可是顯然,藉助遞歸實現的深拷貝若是要克隆層級不少的複雜對象,容易形成內存溢出,咱能夠作出一個小小改進:緩存

看起來酷一點的深拷貝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var deepClone = function(currobj){
    if(typeof currobj !== 'object'){
        return currobj;
    }
    if(currobj instanceof Array){
        var newobj = [];
    }else{
        var newobj = {}
    }
    var currQue = [currobj], newQue = [newobj]; //關鍵在這裏
    while(currQue.length){
        var obj1 = currQue.shift(),obj2 = newQue.shift();
        for(var key in obj1){
            if(typeof obj1[key] !== 'object'){
                obj2[key] = obj1[key];
            }else{
                if(obj1[key] instanceof Array ){
                    obj2[key] = [];
                }else{
                    obj2[key] = {}
                };
                // 妙啊
                currQue.push(obj1[key]);
                newQue.push(obj2[key]);
            }
        }
    }
    return newobj;
};

這裏利用了兩個隊列,還算優雅的避免了遞歸的弊端。函數

JSON序列化

還有一種方法是利用JSON的內置方法,即所謂的JSON序列化:學習

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var deepClone = function(obj){
    var str, newobj = obj.constructor === Array ? [] : {};
    if(typeof obj !== 'object'){
        return;
    } else if(window.JSON){
        str = JSON.stringify(obj), //系列化對象
        newobj = JSON.parse(str); //還原
    } else {
        for(var i in obj){
            newobj[i] = typeof obj[i] === 'object' ? 
            deepClone(obj[i]) : obj[i]; 
        }
    }
    return newobj;
};

不過不打緊,它與上面方法的效果基本相同。this

上面幾種深拷貝的侷限

拜託,你們都很懂對象,上面的方法有幾個很大的問題:prototype

  • 遇到對象內部的循環引用直接gg
  • 沒法拷貝函數(typeof 函數 獲得的是 ‘function’),函數還是引用類型
  • 沒法正確保留實例對象的原型

因而,咱們就要開始改造上面的深拷貝方法來進行完美的克隆了!………….麼?code

等下,你到底要啥

克隆克隆,咱們日常把它掛在嘴上,但面對一個對象,咱們真正想克隆的是什麼?我想在99%的狀況下,咱們想克隆的是對象的數據,而保留它的原型引用方法引用,所以上面提到的侷限中的第二點,基本能夠不考慮。如今咱再來看看怎麼解決剩下兩點。對象

解決循環引用

首先搞清什麼是循環引用,常見的循環引用有兩種:遞歸

自身循環引用

1
2
var a = {};
a._self = a;

這種循環引用能夠說非常常見。隊列

多個對象互相引用

1
2
3
4
var a = {};
var b = {};
a.brother = b;
b.brother = a;

也不是沒見過,不過這是典型致使對象內存沒法被回收的寫法,自己就不推薦。

解決之道

目前只找到了針對第一種引用的解決方法,來自於Jquery源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
jQuery.extend = jQuery.fn.extend = function() {
  // options是一個緩存變量,用來緩存arguments[i]
  // name是用來接收將要被擴展對象的key
  // src改變以前target對象上每一個key對應的value
  // copy傳入對象上每一個key對應的valu
  // copyIsArray斷定copy是否爲一個數組
  // clone深拷貝中用來臨時存對象或數組的src
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1,
  length = arguments.length,
  deep = false;

  // 處理深拷貝的狀況
  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;
};

解決原型的引用

在咱們想辦法魔改深拷貝時,先看下以上這麼多深拷貝的基本原理:

利用for-in循環遍歷對象屬性,若是屬性值是對象則深拷貝,不是則直接賦值

因而俺眉頭一皺發現事情並不簡單,俺上一篇博客已經說明:for-in遍歷的是對象以及其原型鏈上可枚舉屬性,所以想在遍歷時對源對象的__proto__作手腳是根本不存在的,__proto__以及它的不可枚舉屬性根本不會被遍歷到。能夠經過下面的例子看出:

1
2
3
4
5
6
7
8
9
10
11
12
13
var deepClone = function() {...} // 隨便從上面拿一個
var A = function() {
  this.val = 1;
}
A.prototype.log = function() {
  console.log(this.val);
}

var obj1 = new A();
var obj2 = deepClone(obj1);

console.log(obj1); // A {val: 1}
console.log(obj2); // {val: 1, log: function(){...}}

所以,一個解決方法很單純,就是像上面的jQuery.extend方法同樣,本身傳入拷貝的目標對象,extend方法本質上只是拓展目標對象的屬性,使其得到源對象上的數據,這樣一來只要咱們先建立好符合需求的目標對象便可。

另外一種方法則是不採用深拷貝,直接取出須要進行拷貝的對象的數據,而後再利用這份數據來實例化和設置一個新的對象出來

1
2
3
4
5
6
7
8
9
10
11
var Foo = function( obj ){
    this.name = obj.name;
    this.sex = obj.sex
};

Foo.prototype.toJSON = funciton(){
    return { name: this.name, sex: this.sex };
};

var foo = new Foo({ name: "bandit", sex: "male" });
var fooCopy = new Foo( foo.toJSON() );

問題一樣獲得解決【鼓掌】


回顧一下,沒有哪一種方法是萬用的魔法 —— 在咱們想要得到一個克隆對象以前,或許最好先搞清楚咱們究竟是在克隆什麼,再採用最適合的方法。而非是拘泥於「深拷貝淺拷貝」的說法,去複製一段代碼祈禱他能生效。我相信以上的示例代碼尚未考慮到克隆對象的全部問題,但它們在合適的場景下可以處理合適的問題。嗯,其實不少事情都是這樣蛤【帶!】

相關文章
相關標籤/搜索