JavaScript数组与对象拷贝

在我不长的开发经历中,常常在js中遇到数组或对象拷贝的情况,总是不能写对。 于是决定好好梳理一遍相关的知识。

预备知识: 引用类型

ECMAScript 变量可能包含两种不同数据类型的值: 基本类型值和引用类型值 。

当从一个变量向另一个变量复制引用类型的值时,实际复制的是指针,指向存储在堆中的同一个对象。因此改变其中一个变量,就会影响另一个变量。

——《JavaScript高级程序设计(第三版)》

数组Array和对象Object同属于引用类型,因此在拷贝时需要额外的工作,而不能简单的按照 c=a , a=b, b=c的方式来赋值。

数组拷贝

数组的浅拷贝就是直接赋值

var a = [1,2,3];
var b = a; // b = [1,2,3]
b[0] = 10; // b = [10,2,3], a = [10,2,3]

引用类型传递的是地址,因此修改数组b后数组a也会跟着改变。

  • 改进办法1 Array.splice( ) 方法

    数组类型的splice( )方法本来时用于切割数组的,接收两个参数,起点与终点索引值,将这个区间内的数组元素<b>浅拷贝</b>到一个新数组并返回,原数组保持不变。 若不传递任何参数给splice( )方法,则会默认<b>浅拷贝</b>数组中的每一个元素到一个新数组并返回。由于是复制的数组元素而非数组变量本身,因此传递的是数组中每一个元素的值或地址。

    var a = [1,2,3];
    var b = a.splic(); //b = [1,2,3]
    b[0] = 10;         //b = [10,2,3], a = [1,2,3]
    

    这个办法具有局限性,因为其终究是浅拷贝,如果数组中的元素含有引用类型,那么改变新数组中它的值仍会反应到原数组中:

    var obj = { prop: 1};
    var a = [obj,1,2,3]; // a = [{prop:1},1,2,3]
    var b = a.splice();  // b = [{prop:1},1,2,3]
    b[0].prop = 20;
    b[1] = 10;           // b = [{prop:20},10,2,3]
                         // a = [{prop:20},1,2,3]
    

    另外,当被拷贝数组中包含数组元素时(类似二维数组),使用该方法复制将返回空数组。

  • 改进办法2 Array . concat( ) 方法

    数组类型的concat( ) 方法主要用于连接多个数组返回一个新数组,若不带参数则返回一个原数组的拷贝,遗憾的是这个拷贝仍为浅拷贝。与splice( ) 方法表现稍不同的是,若被复制数组中包含数组元素,它仍将正常工作,返回浅拷贝。

    var a = [1,2,3];
    var b = [4,5];
    var c = [a,b];      //c = [[1,2,3],[4,5]];
    var d = c.concat(); //d = [[1,2,3],[4,5]];
    var e = c.splice(); //e = [];
    d[1][1] = 10;       //d = [[1,2,3],[4,10]];
                        //c = [[1,2,3],[4,10]]
    

对象拷贝

直接赋值仍然是浅拷贝,要做到对象的深拷贝,需要将属性一一拷贝到新对象上:

var a = {prop1:1, prop2:2, prop3:3};
//声明一个函数执行深拷贝
function deepCopy(obj){      //接收待拷贝的对象最为参数
  var copy = {};             //声明新副本对象
  for(prop in obj){          // for in语句遍历对象属性
    copy[prop] = obj[prop]   // for in 语句中遍历结果为属性的字符串名,因此需以[]方式访问
  }
  return copy;               //返回新对象
}

var b = deepCopy(a);         // b = {prop1:1,prop2:2,prop3:3}
b.prop1 = 10;                // b = {prop1:10,prop2:2,prop3:3}
                             // a = {prop1:1, prop2:2,prop3:3}

上述这总解决办法只能拷贝所有属性均为基本类型值的对象,如果拷贝的对象其属性中包含了Object或Array等引用类型,仍然会出现指向同一个地址的情况(虽然对象本身没有指向同一个地址了,但是这些引用类型属性仍然指向了同一个地址)。考虑到对象属性层层嵌套的情况并不少见,因此deepCopy( )函数仍然需要优化:

function deepCopy(obj) {
  let copy = {};
  for (prop in obj) {
    if (obj[prop] instanceof Object) {  //先判断该属性是否也是一个Object对象
      copy[prop] = deepCopy(obj[prop]); //若为对象则对该属性再递归调用deepCopy()赋值给新对象的该属性
    } else {
      copy[prop] = obj[prop]            //若该属性不为Object类型,则直接赋值即可
    }
  }
  return copy;        //返回拷贝出的新对象
}

到这里,上述这些方法函数已经完全能够解决我遇到的所有情景了。但其实这个问题还可以推得很深推得很细,下面就来做点"无用功", 再往前迈进一步。

混合拷贝

如前所述,数组的拷贝方法在数组元素为Object或者Array类型时仍然有无法做到副本与原变量独立。 再加上JavaScript “独特”的语言特性,数组与对象可以做到 “你中有我,我中有你” 的混合状态,因此可以再次改进deepCopy( ) ,让它能实现混合情景下的深拷贝。

function deepCopy(origin){
        let copy = null;   //声明副本
        if(origin.constructor === Array){   //确定副本的类型
            copy = [];
        }else if(origin.constructor === Object){
            copy = {};
        }
        for(i in origin){               //遍历原变量的枚举属性
            if(origin[i] instanceof Object){   //Array和Object类型其原型链上均含Object构造函数
                copy[i] = deepCopy(origin[i])  
            }else{
                copy[i] = origin[i];
            }
        } 
        return copy;
    }

现在,deepCopy( ) 可以轻松应对任何包含 数组或者对象的深拷贝了。

虽然 MDN不推荐用for in遍历数组,因为其返回的是索引数字的字符串值,并且在不同的ECMA-262标准中返回的属性或索引值顺序是不同的,不一定按照对象构造时的属性顺序或数组索引值的大小顺序。但是这里拷贝数组则不用担心,虽然不按顺序返回索引值,但最终还是遍历了整个数组。 这样对象分支和数组分支就合并在了同一个for in语句中,代码变得更为简洁。


参考网站: MDN-JavaScript

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 《ijs》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 10...
    叶染柒丶阅读 5,068评论 0 7
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile丽语阅读 3,826评论 0 6
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,204评论 0 4
  • 1. 等闲变却故人心,却道故人心易变。 来年陌生的,是昨日最亲的某某某。 2. 记得以前临近毕业的时候,班里总是会...
    我在和谁说晚安阅读 536评论 1 2
  • 北大新任校长王恩哥甫上任,便向学生提出十句话,在全校引起热议,有学生形容是新的校训。这十句话传播神速,在不少年轻人...
    莹火石头dy阅读 255评论 0 2