尬谈Js对象的深拷贝与浅拷贝

知乎文章地址:https://zhuanlan.zhihu.com/p/44944466

JS 中的浅拷贝与深拷贝,只是针对复杂数据类型(Object,Array)的复制问题。浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了:拷贝引用和拷贝实例,这也是浅拷贝和深拷贝的区别。

  • 浅拷贝:浅拷贝是拷贝引用,拷贝后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响;
  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响;

一.浅拷贝

浅拷贝分两种情况,一是直接拷贝源对象的引用,二是源对象拷贝实例,但其属性(类型为Object、Array的属性)拷贝引用。

拷贝源对象的引用

这是最简单的浅拷贝。例:

var a = {c:1};
var b = a;
console.log(a === b);//输出true
a.c = 2;
console.log(b.c);//输出2

源对象拷贝实例,其属性对象拷贝应用

这种情况下,外层源对象是拷贝实例,如果其属性元素为复杂数据类型时,内层元素拷贝引用。

对于对象实例的拷贝,常用的方法有:Array.prototype.slice(), Array.prototype.concat(), jQuery的$.extend({},obj)

例:

var a = [{c:1}, {d:2}];
var b = a.slice();
console.log(a === b); // 输出false,说明外层数组拷贝的是实例
a[0].c = 3;
console.log(b[0].c); // 输出 3,说明其元素拷贝的是引用

二.深拷贝

深拷贝后,两个对象,包括其内部的元素互不干扰。常见的方法有JSON.parse(),JSON.stringify(),jquery的$.extend(true,{}, obj),lodash的_.cloneDeep和_.clone(value, true)。例:

var a = {c: {d: 1}};
var b = $.extend(true, {}, a);
console.log(a === b); // 输出false
a.c.d = 3;
console.log(b.c.d); // 输出 1,没有改变。

上面的方法中通过JSON.parse()和JSON.stringify()来进行深拷贝是有问题的,来看几个例子:

例1:如果源对象的属性不存在function

var a = {
  name: 'yaodebian',
  info: {
    age: 21,
    weight: 150
  },
  arr: [1, 2, 3]
};
var temp = JSON.stringify(a);
var b = JSON.parse(temp);
a.name = 'I am your father';
a.info.age = 0;
a.arr.push(4);
console.log(a);
console.log(b);

输出结果:

{ name: 'I am your father',
  info: { age: 0, weight: 150 },
  arr: [ 1, 2, 3, 4 ] }
{ name: 'yaodebian',
  info: { age: 21, weight: 150 },
  arr: [ 1, 2, 3 ] }

例2:如果对象图中存在function

var a = {
  name: 'yaodebian',
  info: {
    age: 21,
    weight: 150
  },
  missYou() {
    console.log('I miss you! Are you missing me, too?')
  },
  arr: [1, 2, 3]
};
var temp = JSON.stringify(a);
var b = JSON.parse(temp);
a.name = 'I am your father';
a.info.age = 0;
a.arr.push(4);
console.log(a);
console.log(b);

输出结果:

{ name: 'I am your father',
  info: { age: 0, weight: 150 },
  missYou: [Function: missYou],
  arr: [ 1, 2, 3, 4 ] }
{ name: 'yaodebian',
  info: { age: 21, weight: 150 },
  arr: [ 1, 2, 3 ] }

看出什么问题来了吗,function对于JSON内置对象来讲是一个致命伤(同样,正则对象也一样,在JSON.parse解析时会发生错误)。

接下来,如果让你自己来写一个方法进行深拷贝,你应该怎么来写呢?

下面是网上淘来的一个方法:

var clone = function(obj) {
  if(obj === null) return null;
  if(typeof obj !== 'object') return obj;
  if(obj.cunstructor === Date) return new Date(obj);
  var newObj = new obj.constructor(); //保持继承链
  for(var key in obj) {
    if(obj.hasOwnProperty(key)) {//不遍历其原型链上的属性
      var val = obj[key];
      newObj[key] = typeof val === 'object' ? arguments.callee(val) : val;//使用arguments.callee解除与函数名的耦合
    }
  }
  return newObj;
}

我们来验证一下是否成功吧:

var a = {
  name: 'yaodebian',
  info: {
    age: 21,
    weight: 150
  },
  missYou() {
    console.log('I miss you! Are you missing me, too?')
  },
  arr: [1, 2, 3]
};
 
var b = clone(a);
a.name = 'I am your father';
a.info.age = 0;
a.arr.push(4);
console.log(a);
console.log(b);
b.missYou();

结果为:

{ name: 'I am your father',
  info: { age: 0, weight: 150 },
  missYou: [Function: missYou],
  arr: [ 1, 2, 3, 4 ] }
{ name: 'yaodebian',
  info: { age: 21, weight: 150 },
  missYou: [Function: missYou],
  arr: [ 1, 2, 3 ] }
I miss you! Are you missing me, too?

OK,验证成功!!!

这里有三点需要注意:

  • 1、用new obj.constructor ()构造函数新建一个空的对象,而不是使用{}或者[],这样可以保持原形链的继承;
  • 2、用obj.hasOwnProperty(key)来判断属性是否来自原型链上,因为for..in..也会遍历其原型链上的可枚举属性。
  • 3、上面的函数用到递归算法,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,需要使用 arguments.callee。

不过,上面的还是有点问题,就是没有考虑正则对象,以下给出优化方法:

var clone = function (obj) { 
    if(obj === null) return null 
    if(typeof obj !== 'object') return obj;
    if(obj.constructor===Date) return new Date(obj); 
    if(obj.constructor === RegExp) return new RegExp(obj);
    var newObj = new obj.constructor ();  //保持继承链
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {   //不遍历其原型链上的属性
            var val = obj[key];
            newObj[key] = typeof val === 'object' ? arguments.callee(val) : val; // 使用arguments.callee解除与函数名的耦合
        }
    }  
    return newObj;  
}; 

A:大兄弟,告诉你吧,还是有问题的!!!

B:啥问题???不都可以了吗???

A:obj中放个函数试试~~~

B:木有问题啊,可以拷贝啊,结果对象中有那个函数啊!!!

A:好了,不给你扯犊子了,

两个函数属性指向同一内存好吧,function也是对象来着,不过typeof 返回的是“function”~~~

B:so dis na....

闲话到此为止,上代码:

//根据函数字符串获取参数列表字符串
function getArgStr(s) {
  var args = s.split('(')[1].split(')')[0].split(',');
  var argsStr = "";
  if(args[0] != '') {
    for(var i in args) {
      if(args[i].indexOf("'") != -1) {
        args[i] = args[i].replace(/'/g, '\\\'');
      }
      if(args[i].indexOf('"') != -1) {
        args[i] = args[i].replace('"', "\\\"");
      }
      argsStr += "'" + args[i] + "',";
    }
  }
  return argsStr;
}
 
//根据函数字符串获取函数体字符串
function getFuncBody(s) {
  var body = s.split('{')[1].split('}')[0].split('\n').join(";").replace(/[\r\n]/g, '');
  var temp = body.split('');
  for(var i in temp) {
    if(temp[i] === "'" || temp[i] === '"') {
      temp[i] = "\\" + temp[i];
    }
  }
  body = temp.join('');
  return body;
}
 
//深拷贝
var clone = function(obj) {
  if(obj === null) return null;
  if(typeof obj === 'function') {
    var s = obj.toString();
    var argsStr = getArgStr(s);
    var body = getFuncBody(s);
    return eval("new Function(" + argsStr + "\"" + body + "\");");
  }
  if(typeof obj !== 'object') return obj;
  if(obj.cunstructor === Date) return new Date(obj);
  if(obj.constructor === RegExp) return new RegExp(obj);
  var newObj = new obj.constructor(); //保持继承链
  for(var key in obj) {
    if(obj.hasOwnProperty(key)) {//不遍历其原型链上的属性
      var val = obj[key];
      newObj[key] = (typeof val === 'object' || typeof val === 'function') ? arguments.callee(val) : val;//使用arguments.callee解除与函数名的耦合
    }
  }
  return newObj;
}

思路是这样的,普通方法是不能深拷贝function的,俺也是瞧见Function甚是有几分姿色,顿时生起了。。。(前方高能,胆小勿入,嘻嘻。。。):

  • 1. 首先分析函数的形式,一般函数有以下两种形式:
  • 匿名函数式:function() {};
  • 函数名式:I_Miss_You() {};
  • 我的妈耶,忘记还有es6的形式了。。。算了,之后俺再补下~~~
  • 上面两种形式均包含()和{},我们可以通过这两块获取到相应的参数列表字符串和函数体;
  • 2. 通过函数我们先获取函数字符串即“xxx函数.toString()”;
  • 3. 通过上面获取的字符串获得参数列表字符串和函数体;
  • 4. 根据获得的参数列表字符串和函数体拼接函数实例化字符串,比如:“new Function(...)”,Function具体使用方式百度去!!!
  • 5. 通过eval函数解析并执行步骤4中获取的字符串,返回一个函数实例,拷贝到此为止。

现在,让我们来试试上面的终极方案(少了分析es6函数形式,下次再更新最终极方案好吧~~~):

var a = {
  name: 'yaodebian',
  info: {
    age: 21,
    weight: 150
  },
  missYou(num, temp = "1") {
    console.log("I miss you! Are you missing me, too?")
    console.log(num, temp);
  },
  arr: [1, 2, 3]
};
 
var b = clone(a);
a.name = 'I am your father';
a.info.age = 0;
a.arr.push(4);
console.log(a);
console.log(b);
b.missYou(22);
 
console.log(a.missYou === b.missYou)

输出结果为:

{ name: 'I am your father',
  info: { age: 0, weight: 150 },
  missYou: [Function: missYou],
  arr: [ 1, 2, 3, 4 ] }
{ name: 'yaodebian',
  info: { age: 21, weight: 150 },
  missYou: [Function: anonymous],
  arr: [ 1, 2, 3 ] }
I miss you! Are you missing me, too?
22 '1'
false

完事,溜了溜了~~~

对了,提醒注意一下:replace函数的使用如果没有用原变量来接收函数返回的值,而即仅仅如“str.replace(xxA, xxB)”,这样是没用的(不能改变原来的值,只返回值,不改变值),必须用原变量变量来接收,举栗举栗吧:(我擦,当时俺还捣鼓了半天,没想到是不能改变值,俺的智商莫非还有待提高吗???

)

错误方式:

var str = 'hey yaodebian, you are so ugly';
str.replace('you are so ugly', 'I love you, could you please become my boyfriend!!! 害羞害羞~~~');
console.log(res);//hey yaodebian, you are so ugly

看到没,你还是这么丑

正确方式:

var str = 'hey yaodebian, you are so ugly';
str = str.replace('you are so ugly', 'I love you, could you please become my boyfriend!!! 害羞害羞~~~');
console.log(str);//hey yaodebian, I love you, could you please become my boyfriend!!! 害羞害羞~~~

你看,别人向你表白了~~~

老实说,不是我吹,俺觉得俺还是有几分姿色的~~~

哈哈哈哈~~

 

最后,谢谢两位大佬的文章:

http://yuanhehe.cn/2016/11/03/%E7%90%86%E8%A7%A3JS%E7%9A%84%E6%B5%85%E6%8B%B7%E8%B4%9D%E4%B8%8E%E6%B7%B1%E6%8B%B7%E8%B4%9D/​yuanhehe.cn

 

JS中如何进行对象的深拷贝 - 王玉略 - 博客园​www.cnblogs.com

 

=======================2018.9.26更新======================

之前es6形式的函数没有考虑到,现在仔细琢磨了下,恕本人学识尚浅,主要是当“=>”后面只是一个表达式时,我不知道如何判断它是一个操作还是一个返回值,故而无法解决,倘若有哪位高人知晓,恳请分享,谢谢。

 

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值