摘要:的不能算作深復制,但它至少比直接賦值來得深一些,它創建了一個新的對象。它們的主要用途是對存在環的對象進行深復制。比如源對象中的子對象在深復制以后,對應于。希望這篇文章對你們有幫助深復制方法所謂擁抱未來的深復制實現參考資料
本文最初發布于我的個人博客:咀嚼之味
一年前我曾寫過一篇 Javascript 中的一種深復制實現,當時寫這篇文章的時候還比較稚嫩,有很多地方沒有考慮仔細。為了不誤人子弟,我決定結合 Underscore、lodash 和 jQuery 這些主流的第三方庫來重新談一談這個問題。
第三方庫的實現講一句唯心主義的話,放之四海而皆準的方法是不存在的,不同的深復制實現方法和實現粒度有各自的優劣以及各自適合的應用場景,所以本文并不是在教大家改如何實現深復制,而是將一些在 JavaScript 中實現深復制所需要考慮的問題呈獻給大家。我們首先從較為簡單的 Underscore 開始:
Underscore —— _.clone()在 Underscore 中有這樣一個方法:_.clone(),這個方法實際上是一種淺復制 (shallow-copy),所有嵌套的對象和數組都是直接復制引用而并沒有進行深復制。來看一下例子應該會更加直觀:
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
讓我們來看一下 Underscore 的源碼:
// Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); };
如果目標對象是一個數組,則直接調用數組的slice()方法,否則就是用_.extend()方法。想必大家對extend()方法不會陌生,它的作用主要是將從第二個參數開始的所有對象,按鍵值逐個賦給第一個對象。而在 jQuery 中也有類似的方法。關于 Underscore 中的 _.extend() 方法的實現可以參考 underscore.js #L1006。
Underscore 的 clone() 不能算作深復制,但它至少比直接賦值來得“深”一些,它創建了一個新的對象。另外,你也可以通過以下比較 tricky 的方法來完成單層嵌套的深復制:
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 中也有這么一個叫 $.clone() 的方法,可是它并不是用于一般的 JS 對象的深復制,而是用于 DOM 對象。這不是這篇文章的重點,所以感興趣的同學可以參考jQuery的文檔。與 Underscore 類似,我們也是可以通過 $.extend() 方法來完成深復制。值得慶幸的是,我們在 jQuery 中可以通過添加一個參數來實現遞歸extend。調用$.extend(true, {}, ...)就可以實現深復制啦,參考下面的例子:
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()的實現,也是實現得比較簡潔,而且不太依賴于 jQuery 的內置函數,稍作修改就能拿出來多帶帶使用。
lodash —— _.clone() / _.cloneDeep()在lodash中關于復制的方法有兩個,分別是_.clone()和_.cloneDeep()。其中_.clone(obj, true)等價于_.cloneDeep(obj)。使用上,lodash和前兩者并沒有太大的區別,但看了源碼會發現,Underscore 的實現只有30行左右,而 jQuery 也不過60多行???lodash 中與深復制相關的代碼卻有上百行,這是什么道理呢?
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的深復制?。?! // 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]
通過上面這個例子可以初見端倪,jQuery 無法正確深復制 JSON 對象以外的對象,而我們可以從下面這段代碼片段可以看出 lodash 花了大量的代碼來實現 ES6 引入的大量新的標準對象。更厲害的是,lodash 針對存在環的對象的處理也是非常出色的。因此相較而言,lodash 在深復制上的行為反饋比前兩個庫好很多,是更擁抱未來的一個第三方庫。
/** `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 全局對象
相比于上面介紹的三個庫的做法,針對純 JSON 數據對象的深復制,使用 JSON 全局對象的 parse 和 stringify 方法來實現深復制也算是一個簡單討巧的方法。然而使用這種方法會有一些隱藏的坑,它能正確處理的對象只有 Number, String, Boolean, Array, 扁平對象,即那些能夠被 json 直接表示的數據結構。
function jsonClone(obj) { return JSON.parse(JSON.stringify(obj)); } var clone = jsonClone({ a:1 });擁抱未來的深復制方法
我自己實現了一個深復制的方法,因為用到了Object.create、Object.isPrototypeOf等比較新的方法,所以基本只能在 IE9+ 中使用。而且,我的實現是直接定義在 prototype 上的,很有可能引起大多數的前端同行們的不適。(關于這個我還曾在知乎上提問過:為什么不要直接在Object.prototype上定義方法?)只是實驗性質的,大家參考一下就好,改成非 prototype 版本也是很容易的,不過就是要不斷地去判斷對象的類型了。~
這個實現方法具體可以看我寫的一個小玩意兒——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(); //<----
首先定義一個輔助函數,用于在預定義對象的 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]] }); } }); }
為了避免和源生方法沖突,我在方法名前加了一個 $ 符號。而這個方法的具體實現很簡單,就是遞歸深復制。其中我需要解釋一下兩個參數:srcStack和dstStack。它們的主要用途是對存在環的對象進行深復制。比如源對象中的子對象srcStack[7]在深復制以后,對應于dstStack[7]。該實現方法參考了 lodash 的實現。關于遞歸最重要的就是 Object 和 Array 對象:
/*=====================================* * 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; } });
接下來要針對 Date 和 RegExp 對象的深復制進行一些特殊處理:
/*=====================================* * 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); } });
接下來就是 Number, Boolean 和 String 的 $clone 方法,雖然很簡單,但這也是必不可少的。這樣就能防止像單個字符串這樣的對象錯誤地去調用 Object.prototype.$clone。
/*=====================================* * Number / Boolean / String.prototype * - $clone() *=====================================*/ defineMethods([ Number.prototype, Boolean.prototype, String.prototype ], { "$clone": function() { return this.valueOf(); } });比較各個深復制方法
特性 | jQuery | lodash | JSON.parse | 所謂“擁抱未來的深復制實現” |
---|---|---|---|---|
瀏覽器兼容性 | IE6+ (1.x) & IE9+ (2.x) | IE6+ | IE8+ | IE9+ |
能夠深復制存在環的對象 | 拋出異常 RangeError: Maximum call stack size exceeded | 支持 | 拋出異常 TypeError: Converting circular structure to JSON | 支持 |
對 Date, RegExp 的深復制支持 | × | 支持 | × | 支持 |
對 ES6 新引入的標準對象的深復制支持 | × | 支持 | × | × |
復制數組的屬性 | × | 僅支持RegExp#exec返回的數組結果 | × | 支持 |
是否保留非源生對象的類型 | × | × | × | 支持 |
復制不可枚舉元素 | × | × | × | × |
復制函數 | × | × | × | × |
為了測試各種深復制方法的執行效率,我使用了如下的測試用例:
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);
下面來看看各個實現方法的具體效率如何,我所使用的瀏覽器是 Mac 上的 Chrome 43.0.2357.81 (64-bit) 版本,可以看出來在3次的實驗中,我所實現的方法比 lodash 稍遜一籌,但比jQuery的效率也會高一些。希望這篇文章對你們有幫助~
深復制方法 | jQuery | lodash | JSON.parse | 所謂“擁抱未來的深復制實現” |
---|---|---|---|---|
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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92333.html
摘要:針對本話題,我在年月發布了新的文章深入剖析的深復制要實現深復制有很多辦法,比如最簡單的辦法有上面這種方法好處是非常簡單易用,但是壞處也顯而易見,這會拋棄對象的,也就是深復制之后,無論這個對象原本的構造函數是什么,在深復制之后都會變成。 針對本話題,我在2015年5月發布了新的文章:深入剖析 JavaScript 的深復制 要實現深復制有很多辦法,比如最簡單的辦法有: var...
摘要:還記得剛開始學習的時候,內存管理前端掘金作為一門高級語言,并不像低級語言那樣擁有對內存的完全掌控。第三方庫的行代碼內實現一個前端掘金前言本文會教你如何在行代碼內,不依賴任何第三方的庫,用純實現一個。 (譯) 如何使用 JavaScript 構建響應式引擎 —— Part 1:可觀察的對象 - 掘金原文地址:How to build a reactive engine in JavaSc...
摘要:還記得剛開始學習的時候,內存管理前端掘金作為一門高級語言,并不像低級語言那樣擁有對內存的完全掌控。第三方庫的行代碼內實現一個前端掘金前言本文會教你如何在行代碼內,不依賴任何第三方的庫,用純實現一個。 (譯) 如何使用 JavaScript 構建響應式引擎 —— Part 1:可觀察的對象 - 掘金原文地址:How to build a reactive engine in JavaSc...
摘要:中具有兩種數據類型的值,分別是基本類型值和引用類型值。在中,基本類型值指的是簡單的數據段,引用類型值指那些可能由多個值構成的對象?;緮祿愋突緮祿愋臀炊x的值的默認值尚未存在的對象數字字符串。 整理以及總結一下,回溯下基礎。 ECMAScript中具有兩種數據類型的值,分別是 基本類型值和引用類型值。 在ECMAScript中,基本類型值指的是簡單的數據段,引用類型值指那些可能由...
閱讀 2843·2023-04-26 01:02
閱讀 1863·2021-11-17 09:38
閱讀 791·2021-09-22 15:54
閱讀 2899·2021-09-22 15:29
閱讀 888·2021-09-22 10:02
閱讀 3432·2019-08-30 15:54
閱讀 2007·2019-08-30 15:44
閱讀 1586·2019-08-26 13:46