先談談深拷貝
如何在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; }; |
不過不打緊,它與上面方法的效果基本相同。
上面幾種深拷貝的局限
拜托,大家都很懂對象,上面的方法有幾個很大的問題:
- 遇到對象內部的循環引用直接gg
- 無法拷貝函數(typeof 函數 得到的是 ‘function’),函數仍是引用類型
- 無法正確保留實例對象的原型
于是,我們就要開始改造上面的深拷貝方法來進行完美的克隆了!………….么?
等下,你到底要啥
克隆克隆,我們平常把它掛在嘴上,但面對一個對象,我們真正想克隆的是什么?我想在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深拷貝中用來臨時存對象或數組的srcvar 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() ); |
問題同樣得到解決【鼓掌】
回顧一下,沒有哪種方法是萬用的魔法 —— 在我們想要獲得一個克隆對象之前,或許最好先搞清楚我們到底是在克隆什么,再采用最適合的方法。而非是拘泥于“深拷貝淺拷貝”的說法,去復制一段代碼祈禱他能生效。我相信以上的示例代碼還沒有考慮到克隆對象的所有問題,但它們在合適的場景下能夠處理合適的問題。嗯,其實很多事情都是這樣蛤【帶!】