Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

javaScript中浅拷贝和深拷贝的实现 #3

Open
wengjq opened this issue Feb 13, 2017 · 43 comments
Open

javaScript中浅拷贝和深拷贝的实现 #3

wengjq opened this issue Feb 13, 2017 · 43 comments
Labels

Comments

@wengjq
Copy link
Owner

wengjq commented Feb 13, 2017

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

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 a = {name : b};
            // var b = {name : a}
            // var c = $.extend(a, b);
            // console.log(c);
            // 如果没有这个判断变成可以无限展开的对象
            // 加上这句判断结果是 {name: undefined}
        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、自己动手实现一个拷贝方法

(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 $;
})(window.$ || (window.$ = {}));//类型判断

function copy (obj,deep) { 
    if ($.isFunction(obj)) {
    	return new Function("return " + obj.toString())();
    } else if (obj === null || (typeof obj !== "object")) { 
        return obj; 
    } else {
        var name, target = $.isArray(obj) ? [] : {}, value; 

        for (name in obj) { 
            value = obj[name]; 

            if (value === obj) {
            	continue;
            }

            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value,deep);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                } else {
            	    target[name] = value;
                } 
            } else {
            	target[name] = value;
            } 
        } 
        return target;
    }         
}
@wengjq wengjq changed the title javaScript中的浅拷贝和深拷贝 javaScript中的浅拷贝和深拷贝的实现 Feb 14, 2017
@wengjq wengjq changed the title javaScript中的浅拷贝和深拷贝的实现 javaScript中浅拷贝和深拷贝的实现 Feb 14, 2017
@wengjq wengjq added the 其他 label Mar 5, 2017
@zhoujiamin
Copy link

if(obj === null || typeof obj !== "object"){
return obj;
}
这里是不是没考虑函数呢?
函数也应该像对象一样被考虑吧

@wengjq
Copy link
Owner Author

wengjq commented Jun 26, 2017

@zhoujiamin 之前漏了,已修复哈。

@Ts799498164
Copy link

在“自己动手实现一个拷贝方法”中,有一个小bug。
代码封装了$.isObject方法,注意该方法处理Array类型时是会返回false的。

在if语句中判断时会出现一点小问题:
if(obj === null || (!$.isObject(obj) && !$.isFunction(obj))){ return obj; }
当obj传入Array类型时会直接返回obj本身。
建议修改为
if(obj === null || typeof obj !== 'object'){ return obj; }

文章写的很好,学到很多,谢谢分享。

@wengjq
Copy link
Owner Author

wengjq commented Aug 4, 2017

@Ts799498164 已修复,多谢指正。

@VimMing
Copy link

VimMing commented Aug 22, 2017

@wengjq 有个bug,

  function type() {
    return Object.prototype.toString.call(this).slice(8, -1);
  }

type.call(undefined);
// 结果为 "Window", 所以 isUndefined 总是会返回false

@qw789
Copy link

qw789 commented Aug 23, 2017

对象直接用等号和用shallowClone(copyObj),Object.assign有什么区别呢?

@mrxf
Copy link

mrxf commented Aug 23, 2017

@qw789 对象等号赋值是地址引用,修改其中一个,另一个的值也会随之改变。

@qw789
Copy link

qw789 commented Aug 23, 2017

@mrxf 是的,但是浅复制呢?和对象等号赋值有区别?

@mrxf
Copy link

mrxf commented Aug 23, 2017

浅拷贝是和深拷贝比较的,深拷贝可以理解为递归进行浅拷贝。
深拷贝为了解决对象中依旧含有对象,例如

let a = {
  name: 'LiHua',
  habits:  ['a', 'b']
}
  1. 普通的=赋值:b=a,b如果修改了b.name,那么a.name也会改变
  2. 浅拷贝,b,如果修改了b.name, a.name不会改变,但是修改b.habits数组中的值,a.habits的值也会改变
  3. 深拷贝,b的值改变,不会对a产生任何影响

@qw789
Copy link

qw789 commented Aug 23, 2017

@mrxf 那浅拷贝是不是意味着只能让第一层互相互相不影响。普通等于赋值连第一层都做不到。。

@qw789
Copy link

qw789 commented Aug 23, 2017

@mrxf 谢谢。我大概明白了

@wengjq
Copy link
Owner Author

wengjq commented Aug 23, 2017

@VimMing 已修复,多谢指正。

@wengjq wengjq added 原理 and removed 其他 labels Oct 17, 2017
@vq0599
Copy link

vq0599 commented Nov 22, 2017

你好。
传入的参数为函数的情况,你已经考虑到了。

if ($.isFunction(obj)) {
  return new Function("return " + obj.toString())();
}

但是为什么没有考虑某个属性为函数的情况呢?函数非基本数据类型,不可直接赋值的。

@gzwgq222
Copy link

for下面存在循环引用,用什么方法比较好? 看到了你说JSON在循环引用下是无法正确处理的。
目前遇到个需要多层次循环引用的需求,循环的数组是一个引用地址,改变一个全部都改变了。
求教

@wengjq
Copy link
Owner Author

wengjq commented Dec 14, 2017

@gzwgq222 直接用lodash

@gzwgq222
Copy link

@wengjq 你这个方法可行不?

@yangchongduo
Copy link

行不行 如果能够copy jq的元素就没问题

@lizhongzhen11
Copy link

首先非常感谢作者大大的付出,让我学到了不少,尤其是$,extend(true,{},obj),以前都没接触过,项目做得少,实践的知识也就少,在作者这里倒是好好了解了一番,感谢!

@xiaobinwu
Copy link

厉害

@caistrong
Copy link

caistrong commented Feb 13, 2018

            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value,deep);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                }
            } else {
            	target[name] = value;
            }

感觉这里的逻辑有点问题。如果deep为false的话是没错的,但是如果deep为true的话,当value值是基本类型的值的话,没有target[name] = value。同时感觉这里判断$.isFunction(value)这个步骤我觉得是多余的,因为在你递归调用copy的时候也会判断,function的情况可以合并到上面去。

因为自己复制你的代码到控制台执行的时候报错了,所以也没有验证,不清楚你的代码是不是真的有bug。以上供博主参考。感谢博主的分享交流

@caistrong
Copy link

 var x = {} 
 var y = {} 
 x.i = y 
 y.i = x  
 var z = copy(x,true)

这种情况下的循环引用好像没解决

@Borkes
Copy link

Borkes commented Feb 23, 2018

for (var i = types.length; i--;) {
	    $['is' + types[i]] = (function (self) {
	        return function (elem) {
	           return type.call(elem) === self;
	        };
	    })(types[i]);
	}

这段这么写应该是因为闭包的原因吧,现在for循环里面用let或者const可以解决,不用使用立即执行函数

for (let i = types.length; i--;) {
        $['is' + types[i]] = function (elem) {
            return type.call(elem) === types[i];
        };
    }

@wengjq
Copy link
Owner Author

wengjq commented Feb 26, 2018

@caistrong 感谢指出错误之处,第一个问题已经更正,第二个问题的话是为了复制对象属性是函数的情况。然后那段循环引用的我还没用深入研究。等我研究了再来答。

@wengjq
Copy link
Owner Author

wengjq commented Feb 26, 2018

@Borkes 嗯嗯。也可以哈

@shayminsky
Copy link

var a = {
  arr: [1, 2, 3]
}
var c = copy(a, true)

执行完之后c 并没有拷贝到a中数组的值,
是不是代码这一块少了deep为true时值类型的复制
if (deep) { if ($.isArray(value) || $.isObject(value)) { target[name] = copy(value,deep); } else if ($.isFunction(value)) { target[name] = new Function("return " + value.toString())(); } else { target[name] = value; } } else { target[name] = value; } }

@wengjq
Copy link
Owner Author

wengjq commented Mar 20, 2018

@shayminsky 已更正

@BenjaminWyp
Copy link

(function(){
    let types = 'Number String Boolean Array Object Date Null Undefined RegExp Function'.split(' ');
    for(let type of types){
        $[`is${type}`] = function(param){
            return type === Object.prototype.toString.call(param).slice(8, -1);   
        }
    }
})(window.$ || (window.$ = {}));

实现数据类型判断这么写是不是更简约一点儿?望指正!

@wengjq
Copy link
Owner Author

wengjq commented Apr 6, 2018

@quokaa 没毛病,版本不一样而已

@XavierShi
Copy link

等号赋值不等于浅拷贝?🤔 还有引用类型不是将指针保存在堆内存中吗,为什么开头说

首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据

@wengjq
Copy link
Owner Author

wengjq commented Apr 10, 2018

@XavierShi

// 简单类型都放在栈(图左)(stack)里
// 引用类型都放在堆(图右)(heap)里
var a = 20;
var b = 'abc';
var c = true;
var d = {m: 20} // 地址假设为0x0012ff7c

可以结合图形理解下:

36943396-aedd612c-1fc3-11e8-81c6-cbfffee712ee

@wangzianan
Copy link

hi,抱歉看到你的代码里有一个小错误。

            if (value === obj) {
            	continue;
            }

这是为了防止拷贝无限循环的,正确的应该为

            if (target === obj) {
            	continue;
            }

@renjimin
Copy link

函数的方法和属性拷贝?

@rtfsc0409
Copy link

rtfsc0409 commented Apr 19, 2018

这一个片段for (var i = types.length; i--;) {
$['is' + types[i]] = (function (self) {
return function (elem) {
return type.call(elem) === self;
};
})(types[i]);
}
可不可以这么些呢?
for(var i = types.length; i-- ;){
$['is' + types[i]] = function(elm){
return type.call(elm) === types[i];
}
}
感觉这么些还简洁些
考虑过后, 还是你的写法好, 我这个写法在函数里直接引用了types变量,形成不必要的闭包, 你的写法是通过立即调用函数将types变量传给形参, 避免了不必要的闭包。

@coconilu
Copy link

你好,很高兴看到你的代码,我把copy函数整理一下,变更了一下代码执行顺序,会不会更好?

function copy (obj,deep) { 
    if ($.isFunction(obj)) {
    	return new Function("return " + obj.toString())();
    } else if (obj === null || typeof obj !== "object"){
        return obj; 
    }else {
        var name, target = $.isArray(obj) ? [] : {}, value; 

        for (name in obj) { 
            value = obj[name]; 

            if (value === obj) {
            	continue;
            }

            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value,deep);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                } else {
            	    target[name] = value;
                } 
            } else {
            	target[name] = value;
            } 
        } 
        return target;
    }         
}

@Lingdeqing
Copy link

if (deep) {
if ($.isArray(value) || $.isObject(value)) {
target[name] = copy(value,deep);
} else if ($.isFunction(value)) {
target[name] = new Function("return " + value.toString())();
} else {
target[name] = value;
}
}
这一段为啥不写成
if(deep){
target[name] = copy(value, deep)
}

@liuyanansiyu
Copy link

if (value === obj) {
continue;
}
想问下,这句话什么意思呢?

@Lingdeqing
Copy link

Lingdeqing commented Aug 11, 2018

@liuyanansiyu
比如说
var obj = {
value: {}
};
obj.value.value= obj;
对象obj的属性value等于对象obj自己,这就形成了循环引用。

@tingtingtinghuang
Copy link

要不要考虑下Date这种类型的深拷贝?

@fengzi2016
Copy link

请问如果属性是函数而且还有参数, return new Function('return' + value.toString())是否没有考虑参数问题呢?
return new Function()时可能需要注意这种方法是在全局作用域,内部变量注意和全局相区分。

@fengzi2016
Copy link

fengzi2016 commented Sep 18, 2018

有参数的方法:

const a = {
    fn:function(s,t) {
        console.log(s);
        console.log(t)
    }
}
const args = a.fn.length;
let str = [];
for(let i = 0 ; i<args;i++) {
   str.push("a"+i);
}
str = str.join(',')
const l = new Function( str,'return '+ a.fn.toString())();
a.fn(1,2); // 1 2

@Lingdeqing
Copy link

这边循环引用是否要考虑祖父节点
https://github.com/Lingdeqing/deepClone/blob/master/deepClone.js

@richardmyu
Copy link

 var x = {} 
 var y = {} 
 x.i = y 
 y.i = x  
 var z = copy(x,true)

这种情况下的循环引用好像没解决

稍作修改的:

(function($) {
        "use strict";

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

        for (let i = types.length; i--; ) {
          $["is" + types[i]] = str =>
            Object.prototype.toString.call(str).slice(8, -1) === types[i];
        }
        return $;
      })(window.$ || (window.$ = {})); //类型判断

      function copy(obj, deep = false, hash = new WeakMap()) {
        if (hash.has(obj)) {
          return hash.get(obj);
        }
        if ($.isFunction(obj)) {
          return new Function("return " + obj.toString())();
        } else if (obj === null || typeof obj !== "object") {
          return obj;
        } else {
          var name,
            target = $.isArray(obj) ? [] : {},
            value;
          hash.set(obj, target);

          for (name in obj) {
            value = obj[name];

            if (deep) {
              if ($.isArray(value) || $.isObject(value)) {
                target[name] = copy(value, deep, hash);
              } else if ($.isFunction(value)) {
                target[name] = new Function("return " + value.toString())();
              } else {
                target[name] = value;
              }
            } else {
              target[name] = value;
            }
          }
          return target;
        }
      }

      var x = {};
      var y = {};
      x.i = y;
      y.i = x;
      var z = copy(x, true);
      console.log(x, z);

      var a = {};
      a.a = a;
      var b = copy(a, true);
      console.log(a, b);

@xgqfrms
Copy link

xgqfrms commented Aug 29, 2019

deepClone

const newObj  = JSON.parse(JSON.stringify(obj));

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests