随笔40-浅拷贝与深拷贝

javascript中的深拷贝和浅拷贝区分以及实现

深拷贝方式:let childrenCopy =  JSON.parse(JSON.stringify(children))

编者:一直没有理解变量按引用传递,最近在做一个项目,发现复制一个对象时,当改变对象新的对象时,原来的对象也发生了变化,原来对象中是按引用传递,它们都是保存在一个内存的同个位置,改变对象时,也改变了引用的地址。但我们往往需要拷贝的是一个新的对象,改变值同时也不改变原来的,这时就需要用到深度拷贝和浅度拷贝了。 浅度拷贝:复制一层对象的属性,并不包括对象里面的为引用类型的数据,当改变拷贝的对象里面的引用类型时,源对象也会改变。 深度拷贝:重新开辟一个内存空间,需要递归拷贝对象里的引用,直到子属性都为基本类型。两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

1、javaScript的变量类型

(1)基本类型: 5种基本数据类型Undefined、Null、Boolean、Number 和 String,变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。 (2)引用类型: 存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

JavaScript存储对象都是存地址的,所以浅拷贝会导致 obj1 和obj2 指向同一块内存地址。改变了其中一方的内容,都是在原来的内存上做修改会导致拷贝对象和源对象都发生改变,而深拷贝是开辟一块新的内存地址,将原对象的各个属性逐个复制进去。对拷贝对象和源对象各自的操作互不影响。

例如:数组拷贝

//浅拷贝,双向改变,指向同一片内存空间

var arr1 = [1, 2, 3];

var arr2 = arr1;

arr1[0] = 'change';

console.log('shallow copy: ' + arr1 + " );  //shallow copy: change,2,3

console.log('shallow copy: ' + arr2 + " );  //shallow copy: change,2,3

浅拷贝和深拷贝的区分

首先深复制和浅复制只针对像 Object, Array 这样的复杂对象的。简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。 抛开jQuery,上代码例子。下面是一个简单的浅复制实现:

varobj={a:1,arr:[2,3]};varshallowObj=shallowCopy(obj);functionshallowCopy(src){vardst={};for(varpropinsrc){if(src.hasOwnProperty(prop)){dst[prop]=src[prop];}}returndst;}

因为浅复制只会将对象的各个属性进行依次复制,并不会进行递归复制,而 JavaScript 存储对象都是存地址的,所以浅复制会导致 obj.arr 和 shallowObj.arr 指向同一块内存地址

导致的结果就是:

shallowObj.arr[1]=5;obj.arr[1]// = 5

而深复制则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。这就不会存在上面 obj 和 shallowObj 的 arr 属性指向同一个对象的问题。

varobj={a:1,arr:[1,2]};varobj2=deepCopy(obj);

需要注意的是,如果对象比较大,层级也比较多,深复制会带来性能上的问题。在遇到需要采用深复制的场景时,可以考虑有没有其他替代的方案。在实际的应用场景中,也是浅复制更为常用。

2、浅拷贝的实现

2.1、简单的引用复制

function shallowClone(copyObj) {

  var obj = {};

  for ( var i in copyObj) {

    obj[i] = copyObj[i];

  }

  return obj;

}

var x = {

  a: 1,

  b: { f: { g: 1 } },

  c: [ 1, 2, 3 ]

};

var y = shallowClone(x);

console.log(y.b.f === x.b.f);    // true

2.2、Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

var x = {

  a: 1,

  b: { f: { g: 1 } },

  c: [ 1, 2, 3 ]

};

var y = Object.assign({}, x);

console.log(y.b.f === x.b.f);    // true

3、深拷贝的实现

3.1、Array的slice和concat方法

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:

如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。

对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

如果向两个数组任一中添加了新元素,则另一个不会受到影响。例子如下:

var array = [1,2,3];

var array_shallow = array;

var array_concat = array.concat();

var array_slice = array.slice(0);

console.log(array === array_shallow); //true

console.log(array === array_slice); //false,“看起来”像深拷贝

console.log(array === array_concat); //false,“看起来”像深拷贝

可以看出,concat和slice返回的不同的数组实例,这与直接的引用复制是不同的。而从另一个例子可以看出Array的concat和slice并不是真正的深复制,数组中的对象元素(Object,Array等)只是复制了引用。如下:

var array = [1, [1,2,3], {name:"array"}];

var array_concat = array.concat();

var array_slice = array.slice(0);

array_concat[1][0] = 5;  //改变array_concat中数组元素的值

console.log(array[1]); //[5,2,3]

console.log(array_slice[1]); //[5,2,3]

array_slice[2].name = "array_slice"; //改变array_slice中对象元素的值

console.log(array[2].name); //array_slice

console.log(array_concat[2].name); //array_slice

3.2、JSON对象的parse和stringify

JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深拷贝。

//例1

var source = { name:"source", child:{ name:"child" } }

var target = JSON.parse(JSON.stringify(source));

target.name = "target";  //改变target的name属性

console.log(source.name); //source

console.log(target.name); //target

target.child.name = "target child"; //改变target的child

console.log(source.child.name); //child

console.log(target.child.name); //target child

//例2

var source = { name:function(){console.log(1);}, child:{ name:"child" } }

var target = JSON.parse(JSON.stringify(source));

console.log(target.name); //undefined

//例3

var source = { name:function(){console.log(1);}, child:new RegExp("e") }

var target = JSON.parse(JSON.stringify(source));

console.log(target.name); //undefined

console.log(target.child); //Object {}

这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。

4、jQuery.extend()方法源码实现

jQuery的源码 - src/core.js #L121源码及分析如下:

jQuery.extend = jQuery.fn.extend = function() { //给jQuery对象和jQuery原型对象都添加了extend扩展方法

  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},

  i = 1,

  length = arguments.length,

  deep = false;

  //以上其中的变量:options是一个缓存变量,用来缓存arguments[i],name是用来接收将要被扩展对象的key,src改变之前target对象上每个key对应的value。

  //copy传入对象上每个key对应的value,copyIsArray判定copy是否为一个数组,clone深拷贝中用来临时存对象或数组的src。

  // 处理深拷贝的情况

  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;

};

jQuery的extend方法使用基本的递归思路实现了浅拷贝和深拷贝,但是这个方法也无法处理源对象内部循环引用,例如:

var a = {"name":"aaa"};

var b = {"name":"bbb"};

a.child = b;

b.parent = a;

$.extend(true,{},a);//直接报了栈溢出。Uncaught RangeError: Maximum call stack size exceeded

5、自己动手实现一个拷贝方法

var $ = (function () {

    'use strict';

    var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');

function type () {

  return Object.prototype.toString.call(this).slice(8, -1);

}

for (var i = types.length; i--;) {

    $['is' + types[i]] = (function (self) {

        return function (elem) {

          return type.call(elem) === self;

        };

    })(types[i]);

}

    return $;

})();//类型判断

function copy (obj,deep) {

    if (obj === null || (typeof obj !== "object" && !$.isFunction(obj))) {

        return obj;

    }

    if ($.isFunction(obj)) {

    return new Function("return " + obj.toString())();

    }

    else {

        var name, target = $.isArray(obj) ? [] : {}, value;

        for (name in obj) {

            value = obj[name];

            if (value === obj) {

            continue;

            }

            if (deep && ($.isArray(value) || $.isObject(value))) {

            target[name] = copy(value,deep);

            }

            else {

            target[name] = value;

            }

        }

        return target;

    }        

}

文章:http://caibaojian.com/javascript-object-clone.html

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

推荐阅读更多精彩内容