摘要:的不能算作深復(fù)制,但它至少比直接賦值來(lái)得深一些,它創(chuàng)建了一個(gè)新的對(duì)象。它們的主要用途是對(duì)存在環(huán)的對(duì)象進(jìn)行深復(fù)制。比如源對(duì)象中的子對(duì)象在深復(fù)制以后,對(duì)應(yīng)于。希望這篇文章對(duì)你們有幫助深復(fù)制方法所謂擁抱未來(lái)的深復(fù)制實(shí)現(xiàn)參考資料
本文最初發(fā)布于我的個(gè)人博客:咀嚼之味
一年前我曾寫(xiě)過(guò)一篇 Javascript 中的一種深復(fù)制實(shí)現(xiàn),當(dāng)時(shí)寫(xiě)這篇文章的時(shí)候還比較稚嫩,有很多地方?jīng)]有考慮仔細(xì)。為了不誤人子弟,我決定結(jié)合 Underscore、lodash 和 jQuery 這些主流的第三方庫(kù)來(lái)重新談一談這個(gè)問(wèn)題。
第三方庫(kù)的實(shí)現(xiàn)講一句唯心主義的話,放之四海而皆準(zhǔn)的方法是不存在的,不同的深復(fù)制實(shí)現(xiàn)方法和實(shí)現(xiàn)粒度有各自的優(yōu)劣以及各自適合的應(yīng)用場(chǎng)景,所以本文并不是在教大家改如何實(shí)現(xiàn)深復(fù)制,而是將一些在 JavaScript 中實(shí)現(xiàn)深復(fù)制所需要考慮的問(wèn)題呈獻(xiàn)給大家。我們首先從較為簡(jiǎn)單的 Underscore 開(kāi)始:
Underscore —— _.clone()在 Underscore 中有這樣一個(gè)方法:_.clone(),這個(gè)方法實(shí)際上是一種淺復(fù)制 (shallow-copy),所有嵌套的對(duì)象和數(shù)組都是直接復(fù)制引用而并沒(méi)有進(jìn)行深復(fù)制。來(lái)看一下例子應(yīng)該會(huì)更加直觀:
var x = { a: 1, b: { z: 0 } }; var y = _.clone(x); y === x // false y.b === x.b // true x.b.z = 100; y.b.z // 100
讓我們來(lái)看一下 Underscore 的源碼:
// Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); };
如果目標(biāo)對(duì)象是一個(gè)數(shù)組,則直接調(diào)用數(shù)組的slice()方法,否則就是用_.extend()方法。想必大家對(duì)extend()方法不會(huì)陌生,它的作用主要是將從第二個(gè)參數(shù)開(kāi)始的所有對(duì)象,按鍵值逐個(gè)賦給第一個(gè)對(duì)象。而在 jQuery 中也有類(lèi)似的方法。關(guān)于 Underscore 中的 _.extend() 方法的實(shí)現(xiàn)可以參考 underscore.js #L1006。
Underscore 的 clone() 不能算作深復(fù)制,但它至少比直接賦值來(lái)得“深”一些,它創(chuàng)建了一個(gè)新的對(duì)象。另外,你也可以通過(guò)以下比較 tricky 的方法來(lái)完成單層嵌套的深復(fù)制:
var _ = require("underscore"); var a = [{f: 1}, {f:5}, {f:10}]; var b = _.map(a, _.clone); // <---- b[1].f = 55; console.log(JSON.stringify(a)); // [{"f":1},{"f":5},{"f":10}]jQuery —— $.clone() / $.extend()
在 jQuery 中也有這么一個(gè)叫 $.clone() 的方法,可是它并不是用于一般的 JS 對(duì)象的深復(fù)制,而是用于 DOM 對(duì)象。這不是這篇文章的重點(diǎn),所以感興趣的同學(xué)可以參考jQuery的文檔。與 Underscore 類(lèi)似,我們也是可以通過(guò) $.extend() 方法來(lái)完成深復(fù)制。值得慶幸的是,我們?cè)?jQuery 中可以通過(guò)添加一個(gè)參數(shù)來(lái)實(shí)現(xiàn)遞歸extend。調(diào)用$.extend(true, {}, ...)就可以實(shí)現(xiàn)深復(fù)制啦,參考下面的例子:
var x = { a: 1, b: { f: { g: 1 } }, c: [ 1, 2, 3 ] }; var y = $.extend({}, x), //shallow copy z = $.extend(true, {}, x); //deep copy y.b.f === x.b.f // true z.b.f === x.b.f // false
在 jQuery的源碼 - src/core.js #L121 文件中我們可以找到$.extend()的實(shí)現(xiàn),也是實(shí)現(xiàn)得比較簡(jiǎn)潔,而且不太依賴于 jQuery 的內(nèi)置函數(shù),稍作修改就能拿出來(lái)多帶帶使用。
lodash —— _.clone() / _.cloneDeep()在lodash中關(guān)于復(fù)制的方法有兩個(gè),分別是_.clone()和_.cloneDeep()。其中_.clone(obj, true)等價(jià)于_.cloneDeep(obj)。使用上,lodash和前兩者并沒(méi)有太大的區(qū)別,但看了源碼會(huì)發(fā)現(xiàn),Underscore 的實(shí)現(xiàn)只有30行左右,而 jQuery 也不過(guò)60多行???lodash 中與深復(fù)制相關(guān)的代碼卻有上百行,這是什么道理呢?
var $ = require("jquery"), _ = require("lodash"); var arr = new Int16Array(5), obj = { a: arr }, obj2; arr[0] = 5; arr[1] = 6; // 1. jQuery obj2 = $.extend(true, {}, obj); console.log(obj2.a); // [5, 6, 0, 0, 0] Object.prototype.toString.call(obj2); // [object Int16Array] obj2.a[0] = 100; console.log(obj); // [100, 6, 0, 0, 0] //此處jQuery不能正確處理Int16Array的深復(fù)制?。。? // 2. lodash obj2 = _.cloneDeep(obj); console.log(obj2.a); // [5, 6, 0, 0, 0] Object.prototype.toString.call(arr2); // [object Int16Array] obj2.a[0] = 100; console.log(obj); // [5, 6, 0, 0, 0]
通過(guò)上面這個(gè)例子可以初見(jiàn)端倪,jQuery 無(wú)法正確深復(fù)制 JSON 對(duì)象以外的對(duì)象,而我們可以從下面這段代碼片段可以看出 lodash 花了大量的代碼來(lái)實(shí)現(xiàn) ES6 引入的大量新的標(biāo)準(zhǔn)對(duì)象。更厲害的是,lodash 針對(duì)存在環(huán)的對(duì)象的處理也是非常出色的。因此相較而言,lodash 在深復(fù)制上的行為反饋比前兩個(gè)庫(kù)好很多,是更擁抱未來(lái)的一個(gè)第三方庫(kù)。
/** `Object#toString` result references. */ var argsTag = "[object Arguments]", arrayTag = "[object Array]", boolTag = "[object Boolean]", dateTag = "[object Date]", errorTag = "[object Error]", funcTag = "[object Function]", mapTag = "[object Map]", numberTag = "[object Number]", objectTag = "[object Object]", regexpTag = "[object RegExp]", setTag = "[object Set]", stringTag = "[object String]", weakMapTag = "[object WeakMap]"; var arrayBufferTag = "[object ArrayBuffer]", float32Tag = "[object Float32Array]", float64Tag = "[object Float64Array]", int8Tag = "[object Int8Array]", int16Tag = "[object Int16Array]", int32Tag = "[object Int32Array]", uint8Tag = "[object Uint8Array]", uint8ClampedTag = "[object Uint8ClampedArray]", uint16Tag = "[object Uint16Array]", uint32Tag = "[object Uint32Array]";借助 JSON 全局對(duì)象
相比于上面介紹的三個(gè)庫(kù)的做法,針對(duì)純 JSON 數(shù)據(jù)對(duì)象的深復(fù)制,使用 JSON 全局對(duì)象的 parse 和 stringify 方法來(lái)實(shí)現(xiàn)深復(fù)制也算是一個(gè)簡(jiǎn)單討巧的方法。然而使用這種方法會(huì)有一些隱藏的坑,它能正確處理的對(duì)象只有 Number, String, Boolean, Array, 扁平對(duì)象,即那些能夠被 json 直接表示的數(shù)據(jù)結(jié)構(gòu)。
function jsonClone(obj) { return JSON.parse(JSON.stringify(obj)); } var clone = jsonClone({ a:1 });擁抱未來(lái)的深復(fù)制方法
我自己實(shí)現(xiàn)了一個(gè)深復(fù)制的方法,因?yàn)橛玫搅?b>Object.create、Object.isPrototypeOf等比較新的方法,所以基本只能在 IE9+ 中使用。而且,我的實(shí)現(xiàn)是直接定義在 prototype 上的,很有可能引起大多數(shù)的前端同行們的不適。(關(guān)于這個(gè)我還曾在知乎上提問(wèn)過(guò):為什么不要直接在Object.prototype上定義方法?)只是實(shí)驗(yàn)性質(zhì)的,大家參考一下就好,改成非 prototype 版本也是很容易的,不過(guò)就是要不斷地去判斷對(duì)象的類(lèi)型了。~
這個(gè)實(shí)現(xiàn)方法具體可以看我寫(xiě)的一個(gè)小玩意兒——Cherry.js,使用方法大概是這樣的:
function X() { this.x = 5; this.arr = [1,2,3]; } var obj = { d: new Date(), r: /abc/ig, x: new X(), arr: [1,2,3] }, obj2, clone; obj.x.xx = new X(); obj.arr.testProp = "test"; clone = obj.$clone(); //<----
首先定義一個(gè)輔助函數(shù),用于在預(yù)定義對(duì)象的 Prototype 上定義方法:
function defineMethods(protoArray, nameToFunc) { protoArray.forEach(function(proto) { var names = Object.keys(nameToFunc), i = 0; for (; i < names.length; i++) { Object.defineProperty(proto, names[i], { enumerable: false, configurable: true, writable: true, value: nameToFunc[names[i]] }); } }); }
為了避免和源生方法沖突,我在方法名前加了一個(gè) $ 符號(hào)。而這個(gè)方法的具體實(shí)現(xiàn)很簡(jiǎn)單,就是遞歸深復(fù)制。其中我需要解釋一下兩個(gè)參數(shù):srcStack和dstStack。它們的主要用途是對(duì)存在環(huán)的對(duì)象進(jìn)行深復(fù)制。比如源對(duì)象中的子對(duì)象srcStack[7]在深復(fù)制以后,對(duì)應(yīng)于dstStack[7]。該實(shí)現(xiàn)方法參考了 lodash 的實(shí)現(xiàn)。關(guān)于遞歸最重要的就是 Object 和 Array 對(duì)象:
/*=====================================* * Object.prototype * - $clone() *=====================================*/ defineMethods([ Object.prototype ], { "$clone": function (srcStack, dstStack) { var obj = Object.create(Object.getPrototypeOf(this)), keys = Object.keys(this), index, prop; srcStack = srcStack || []; dstStack = dstStack || []; srcStack.push(this); dstStack.push(obj); for (var i = 0; i < keys.length; i++) { prop = this[keys[i]]; if (prop === null || prop === undefined) { obj[keys[i]] = prop; } else if (!prop.$isFunction()) { if (prop.$isPlainObject()) { index = srcStack.lastIndexOf(prop); if (index > 0) { obj[keys[i]] = dstStack[index]; continue; } } obj[keys[i]] = prop.$clone(srcStack, dstStack); } } return obj; } }); /*=====================================* * Array.prototype * - $clone() *=====================================*/ defineMethods([ Array.prototype ], { "$clone": function (srcStack, dstStack) { var thisArr = this.valueOf(), newArr = [], keys = Object.keys(thisArr), index, element; srcStack = srcStack || []; dstStack = dstStack || []; srcStack.push(this); dstStack.push(newArr); for (var i = 0; i < keys.length; i++) { element = thisArr[keys[i]]; if (element === undefined || element === null) { newArr[keys[i]] = element; } else if (!element.$isFunction()) { if (element.$isPlainObject()) { index = srcStack.lastIndexOf(element); if (index > 0) { newArr[keys[i]] = dstStack[index]; continue; } } } newArr[keys[i]] = element.$clone(srcStack, dstStack); } return newArr; } });
接下來(lái)要針對(duì) Date 和 RegExp 對(duì)象的深復(fù)制進(jìn)行一些特殊處理:
/*=====================================* * Date.prototype * - $clone *=====================================*/ defineMethods([ Date.prototype ], { "$clone": function() { return new Date(this.valueOf()); } }); /*=====================================* * RegExp.prototype * - $clone *=====================================*/ defineMethods([ RegExp.prototype ], { "$clone": function () { var pattern = this.valueOf(); var flags = ""; flags += pattern.global ? "g" : ""; flags += pattern.ignoreCase ? "i" : ""; flags += pattern.multiline ? "m" : ""; return new RegExp(pattern.source, flags); } });
接下來(lái)就是 Number, Boolean 和 String 的 $clone 方法,雖然很簡(jiǎn)單,但這也是必不可少的。這樣就能防止像單個(gè)字符串這樣的對(duì)象錯(cuò)誤地去調(diào)用 Object.prototype.$clone。
/*=====================================* * Number / Boolean / String.prototype * - $clone() *=====================================*/ defineMethods([ Number.prototype, Boolean.prototype, String.prototype ], { "$clone": function() { return this.valueOf(); } });比較各個(gè)深復(fù)制方法
特性 | jQuery | lodash | JSON.parse | 所謂“擁抱未來(lái)的深復(fù)制實(shí)現(xiàn)” |
---|---|---|---|---|
瀏覽器兼容性 | IE6+ (1.x) & IE9+ (2.x) | IE6+ | IE8+ | IE9+ |
能夠深復(fù)制存在環(huán)的對(duì)象 | 拋出異常 RangeError: Maximum call stack size exceeded | 支持 | 拋出異常 TypeError: Converting circular structure to JSON | 支持 |
對(duì) Date, RegExp 的深復(fù)制支持 | × | 支持 | × | 支持 |
對(duì) ES6 新引入的標(biāo)準(zhǔn)對(duì)象的深復(fù)制支持 | × | 支持 | × | × |
復(fù)制數(shù)組的屬性 | × | 僅支持RegExp#exec返回的數(shù)組結(jié)果 | × | 支持 |
是否保留非源生對(duì)象的類(lèi)型 | × | × | × | 支持 |
復(fù)制不可枚舉元素 | × | × | × | × |
復(fù)制函數(shù) | × | × | × | × |
為了測(cè)試各種深復(fù)制方法的執(zhí)行效率,我使用了如下的測(cè)試用例:
var x = {}; for (var i = 0; i < 1000; i++) { x[i] = {}; for (var j = 0; j < 1000; j++) { x[i][j] = Math.random(); } } var start = Date.now(); var y = clone(x); console.log(Date.now() - start);
下面來(lái)看看各個(gè)實(shí)現(xiàn)方法的具體效率如何,我所使用的瀏覽器是 Mac 上的 Chrome 43.0.2357.81 (64-bit) 版本,可以看出來(lái)在3次的實(shí)驗(yàn)中,我所實(shí)現(xiàn)的方法比 lodash 稍遜一籌,但比jQuery的效率也會(huì)高一些。希望這篇文章對(duì)你們有幫助~
深復(fù)制方法 | jQuery | lodash | JSON.parse | 所謂“擁抱未來(lái)的深復(fù)制實(shí)現(xiàn)” |
---|---|---|---|---|
Test 1 | 475 | 341 | 630 | 320 |
Test 2 | 505 | 270 | 690 | 345 |
Test 3 | 456 | 268 | 650 | 332 |
Average | 478.7 | 293 | 656.7 | 332.3 |
Underscore - clone
Stackoverflow - How do you clone an array of objects using underscore?
jQuery API
lodash docs #clone
MDN - JSON.stringify
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/92333.html
摘要:針對(duì)本話題,我在年月發(fā)布了新的文章深入剖析的深復(fù)制要實(shí)現(xiàn)深復(fù)制有很多辦法,比如最簡(jiǎn)單的辦法有上面這種方法好處是非常簡(jiǎn)單易用,但是壞處也顯而易見(jiàn),這會(huì)拋棄對(duì)象的,也就是深復(fù)制之后,無(wú)論這個(gè)對(duì)象原本的構(gòu)造函數(shù)是什么,在深復(fù)制之后都會(huì)變成。 針對(duì)本話題,我在2015年5月發(fā)布了新的文章:深入剖析 JavaScript 的深復(fù)制 要實(shí)現(xiàn)深復(fù)制有很多辦法,比如最簡(jiǎn)單的辦法有: var...
摘要:總結(jié)綜上所述,數(shù)組的深拷貝比較簡(jiǎn)單,方法沒(méi)有什么爭(zhēng)議,對(duì)象的深拷貝,比較好的方法是用的方法實(shí)現(xiàn),或者遞歸實(shí)現(xiàn),比較簡(jiǎn)單的深復(fù)制可以使用實(shí)現(xiàn)參考資料知乎中的深拷貝和淺拷貝深入剖析的深復(fù)制 深淺復(fù)制對(duì)比 因?yàn)镴avaScript存儲(chǔ)對(duì)象都是存地址的,所以淺復(fù)制會(huì)導(dǎo)致 obj 和obj1 指向同一塊內(nèi)存地址。我的理解是,這有點(diǎn)類(lèi)似數(shù)據(jù)雙向綁定,改變了其中一方的內(nèi)容,都是在原來(lái)的內(nèi)存基礎(chǔ)上做...
摘要:還記得剛開(kāi)始學(xué)習(xí)的時(shí)候,內(nèi)存管理前端掘金作為一門(mén)高級(jí)語(yǔ)言,并不像低級(jí)語(yǔ)言那樣擁有對(duì)內(nèi)存的完全掌控。第三方庫(kù)的行代碼內(nèi)實(shí)現(xiàn)一個(gè)前端掘金前言本文會(huì)教你如何在行代碼內(nèi),不依賴任何第三方的庫(kù),用純實(shí)現(xiàn)一個(gè)。 (譯) 如何使用 JavaScript 構(gòu)建響應(yīng)式引擎 —— Part 1:可觀察的對(duì)象 - 掘金原文地址:How to build a reactive engine in JavaSc...
摘要:還記得剛開(kāi)始學(xué)習(xí)的時(shí)候,內(nèi)存管理前端掘金作為一門(mén)高級(jí)語(yǔ)言,并不像低級(jí)語(yǔ)言那樣擁有對(duì)內(nèi)存的完全掌控。第三方庫(kù)的行代碼內(nèi)實(shí)現(xiàn)一個(gè)前端掘金前言本文會(huì)教你如何在行代碼內(nèi),不依賴任何第三方的庫(kù),用純實(shí)現(xiàn)一個(gè)。 (譯) 如何使用 JavaScript 構(gòu)建響應(yīng)式引擎 —— Part 1:可觀察的對(duì)象 - 掘金原文地址:How to build a reactive engine in JavaSc...
摘要:中具有兩種數(shù)據(jù)類(lèi)型的值,分別是基本類(lèi)型值和引用類(lèi)型值。在中,基本類(lèi)型值指的是簡(jiǎn)單的數(shù)據(jù)段,引用類(lèi)型值指那些可能由多個(gè)值構(gòu)成的對(duì)象?;緮?shù)據(jù)類(lèi)型基本數(shù)據(jù)類(lèi)型未定義的值的默認(rèn)值尚未存在的對(duì)象數(shù)字字符串。 整理以及總結(jié)一下,回溯下基礎(chǔ)。 ECMAScript中具有兩種數(shù)據(jù)類(lèi)型的值,分別是 基本類(lèi)型值和引用類(lèi)型值。 在ECMAScript中,基本類(lèi)型值指的是簡(jiǎn)單的數(shù)據(jù)段,引用類(lèi)型值指那些可能由...
閱讀 2931·2023-04-26 01:02
閱讀 1977·2021-11-17 09:38
閱讀 877·2021-09-22 15:54
閱讀 2958·2021-09-22 15:29
閱讀 956·2021-09-22 10:02
閱讀 3618·2019-08-30 15:54
閱讀 2104·2019-08-30 15:44
閱讀 1657·2019-08-26 13:46