Javascript的对象拷贝

 

作者:疯狂的技术宅来源:前端先锋

在开始之前,我先普及一些基础知识。Javascript 的对象只是指向内存中某个位置的指针。这些指针是可变的,也就是说,它们可以重新被赋值。所以仅仅复制这个指针,其结果是有两个指针指向内存中的同一个地址。

 
  1. var foo = { 
  2.     a : "abc" 
  3. console.log(foo.a); 
  4. // abc 
  5.  
  6. var bar = foo; 
  7. console.log(bar.a); 
  8. // abc 
  9.  
  10. foo.a = "yo foo"; 
  11. console.log(foo.a); 
  12. // yo foo 
  13. console.log(bar.a); 
  14. // yo foo 
  15.  
  16. bar.a = "whatup bar?"; 
  17. console.log(foo.a); 
  18. // whatup bar? 
  19. console.log(bar.a); 
  20. // whatup bar?     

通过上面的例子可以看到,对象 foo 和 bar 都能随着对方的变化而变化。所以在拷贝 Javascript 中的对象时,要根据实际情况做一些考虑。

浅拷贝

如果要操作的对象拥有的属性都是值类型,那么可以使用扩展语法或 Object.assign(...)

 
  1. var obj = { foo: "foo", bar: "bar" }; 
  2. var copy = { ...obj }; 
  3. // Object { foo: "foo", bar: "bar" } 
  4. var obj = { foo: "foo", bar: "bar" }; 
  5. var copy = Object.assign({}, obj); 
  6. // Object { foo: "foo", bar: "bar" } 

可以看到上面两种方法都可以把多个不同来源对象中的属性复制到一个目标对象中。

 
  1. var obj1 = { foo: "foo" }; 
  2. var obj2 = { bar: "bar" }; 
  3. var copySpread = { ...obj1, ...obj2 }; 
  4. // Object { foo: "foo", bar: "bar" } 
  5. var copyAssign = Object.assign({}, obj1, obj2); 
  6. // Object { foo: "foo", bar: "bar" } 

上面这种方法是存在问题的,如果对象的属性也是对象,那么实际被拷贝的只是那些指针,这跟执行 var bar = foo; 的效果是一样的,和第一段代码中的做法一样。

 
  1. var foo = { a: 0 , b: { c: 0 } }; 
  2. var copy = { ...foo }; 
  3. copy.a = 1; 
  4. copy.b.c = 2; 
  5. console.dir(foo); 
  6. // { a: 0, b: { c: 2 } } 
  7. console.dir(copy); 
  8. // { a: 1, b: { c: 2 } } 

深拷贝(有限制)

想要对一个对象进行深拷贝,一个可行的方法是先把对象序列化为字符串,然后再对它进行反序列化。

 
  1. var obj = { a: 0, b: { c: 0 } }; 
  2. var copy = JSON.parse(JSON.stringify(obj)); 

不幸的是,这个方法只在对象中包含可序列化值,同时没有循环引用的情况下适用。常见的不能被序列化的就是日期对象 —— 尽管它显示的是字符串化的 ISO 日期格式,但是 JSON.parse 只会把它解析成为一个字符串,而不是日期类型。

深拷贝 (限制较少)

对于一些更复杂的场景,我们可以用 HTML5 提供的一个名为结构化克隆的新算法。不过,截至本文发布为止,有些内置类型仍然无法支持,但与 JSON.parse 相比较而言,它支持的类型要多的多:Date、RegExp、 Map、 Set、 Blob、 FileList、 ImageData、 sparse 和 typed Array。它还维护了克隆对象的引用,这使它可以支持循环引用结构的拷贝,而这些在前面所说的序列化中是不支持的。

目前还没有直接调用结构化克隆的方法,但是有些新的浏览器特性的底层用了这个算法。所以深拷贝对象可能需要依赖一系列的环境才能实现。

Via MessageChannels: 其原理是借用了通信中用到的序列化算法。由于它是基于事件的,所以这里的克隆也是一个异步操作。

 
  1. class StructuredCloner { 
  2.   constructor() { 
  3.     this.pendingClones_ = new Map(); 
  4.     this.nextKey_ = 0; 
  5.  
  6.     const channel = new MessageChannel(); 
  7.     this.inPort_ = channel.port1; 
  8.     this.outPort_ = channel.port2; 
  9.  
  10.     this.outPort_.onmessage = ({data: {key, value}}) => { 
  11.       const resolve = this.pendingClones_.get(key); 
  12.       resolve(value); 
  13.       this.pendingClones_.delete(key); 
  14.     }; 
  15.     this.outPort_.start(); 
  16.   } 
  17.  
  18.   cloneAsync(value) { 
  19.     return new Promise(resolve => { 
  20.       const key = this.nextKey_++; 
  21.       this.pendingClones_.set(key, resolve); 
  22.       this.inPort_.postMessage({key, value}); 
  23.     }); 
  24.   } 
  25.  
  26. const structuredCloneAsync = window.structuredCloneAsync = 
  27.     StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner); 
  28.  
  29. const main = async () => { 
  30.   const original = { date: new Date(), number: Math.random() }; 
  31.   originaloriginal.self = original; 
  32.  
  33.   const clone = await structuredCloneAsync(original); 
  34.  
  35.   // different objects: 
  36.   console.assert(original !== clone); 
  37.   console.assert(original.date !== clone.date); 
  38.  
  39.   // cyclical: 
  40.   console.assert(original.self === original); 
  41.   console.assert(clone.self === clone); 
  42.  
  43.   // equivalent values: 
  44.   console.assert(original.number === clone.number); 
  45.   console.assert(Number(original.date) === Number(clone.date)); 
  46.  
  47.   console.log("Assertions complete."); 
  48. }; 
  49.  
  50. main(); 

Via the history API:history.pushState() 和 history.replaceState()都会给它们的第一个参数做一个结构化克隆!需要注意的是,此方法是同步的,因为对浏览器历史记录进行操作的速度不是很快,假如频繁调用这个方法,将会导致浏览器卡死。

 
  1. const structuredClone = obj => { 
  2.   const oldState = history.state; 
  3.   history.replaceState(obj, null); 
  4.   const clonedObj = history.state; 
  5.   history.replaceState(oldState, null); 
  6.   return clonedObj; 
  7. }; 

Via notification API:当创建一个 notification 实例的时候,构造器为它相关的数据做了结构化克隆。需要注意的是,它会尝试向用户展示浏览器通知,但是除非它收到了用户允许展示通知的请求,否则它什么都不会做。一旦用户点击同意的话,notification 会立刻被关闭。

 
  1. const structuredClone = obj => { 
  2.   const n = new Notification("", {data: obj, silent: true}); 
  3.   nn.onshow = n.close.bind(n); 
  4.   return n.data; 
  5. }; 

用 Node.js 进行深拷贝

Node.js 的 8.0.0 版本提供了一个 序列化 api 可以和结构化克隆相媲美. 不过这个 API 在本文发布的时候,还只是被标记为试验性的:

 
  1. const v8 = require('v8'); 
  2. const buf = v8.serialize({a: 'foo', b: new Date()}); 
  3. const cloned = v8.deserialize(buf); 
  4. cloned.b.getMonth(); 

在 8.0.0 版本以下比较稳定的方法,可以考虑用 lodash 的 cloneDeep函数,它的思想多少也基于结构化克隆算法。

结论

Javascript 中最好的对象拷贝的算法,很大程度上取决于其使用环境,以及你需要拷贝的对象类型。虽然 lodash 是最安全的泛型深拷贝函数,但是如果你自己封装的话,也许能够获得效率更高的实现方法,以下就是一个简单的深拷贝,对 Date 日期对象也同样适用:

 
  1. function deepClone(obj) { 
  2.   var copy; 
  3.  
  4.   // Handle the 3 simple types, and null or undefined 
  5.   if (null == obj || "object" != typeof obj) return obj; 
  6.  
  7.   // Handle Date 
  8.   if (obj instanceof Date) { 
  9.     copy = new Date(); 
  10.     copy.setTime(obj.getTime()); 
  11.     return copy; 
  12.   } 
  13.  
  14.   // Handle Array 
  15.   if (obj instanceof Array) { 
  16.     copy = []; 
  17.     for (var i = 0, len = obj.length; i < len; i++) { 
  18.         copy[i] = deepClone(obj[i]); 
  19.     } 
  20.     return copy; 
  21.   } 
  22.  
  23.   // Handle Function 
  24.   if (obj instanceof Function) { 
  25.     copy = function() { 
  26.       return obj.apply(this, arguments); 
  27.     } 
  28.     return copy; 
  29.   } 
  30.  
  31.   // Handle Object 
  32.   if (obj instanceof Object) { 
  33.       copy = {}; 
  34.       for (var attr in obj) { 
  35.           if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]); 
  36.       } 
  37.       return copy; 
  38.   } 
  39.  
  40.   throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name); 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Javascript 中可以通过多种方式来实现对象的合并。其中一种常用的方法是使用 `Object.assign()` 函数。 例如: ``` let obj1 = {a: 1, b: 2}; let obj2 = {c: 3, d: 4}; let mergedObj = Object.assign({}, obj1, obj2); console.log(mergedObj); // Output: {a: 1, b: 2, c: 3, d: 4} ``` `Object.assign()` 函数接受多个对象作为参数,并将它们合并到第一个对象(也可以是一个空对象)中,返回合并后的对象。 需要注意的是,如果存在同名属性,后面的对象会覆盖前面的对象的属性值。 ### 回答2: JavaScript中的对象合并是指将两个或多个对象的属性合并到一个新的对象中,以创建一个更大和更完整的对象JavaScript提供了几种方法来合并对象。其中一种常见的方法是使用Object.assign()函数。这个函数接受目标对象和一个或多个源对象作为参数,并将源对象的属性和值合并到目标对象中。如果目标对象已经存在相同的属性,则源对象的值会覆盖目标对象的值。 例如,假设我们有两个对象obj1和obj2: ``` let obj1 = { a: 1, b: 2 }; let obj2 = { c: 3, d: 4 }; ``` 我们可以使用Object.assign()函数将这两个对象合并为一个新的对象: ``` let mergedObj = Object.assign({}, obj1, obj2); ``` 现在,mergedObj将包含所有obj1和obj2的属性和值: ``` { a: 1, b: 2, c: 3, d: 4 } ``` 另一个常见的方法是使用扩展运算符(...)来合并对象。这个运算符可以将一个对象的属性和值展开到另一个对象中。 ``` let obj1 = { a: 1, b: 2 }; let obj2 = { c: 3, d: 4 }; let mergedObj = { ...obj1, ...obj2 }; ``` 这将创建一个新的对象mergedObj,包含了obj1和obj2的所有属性和值。 除了以上两个方法,还可以使用循环遍历对象的属性并依次将其添加到目标对象中来实现对象的合并。 总之,JavaScript提供了多种方法来合并对象,开发者可以根据需求选择最适合的方法来合并对象。 ### 回答3: JavaScript中可以使用Object.assign()方法来合并对象。这个方法接受多个参数,第一个参数是目标对象,后面的参数是源对象。它会将所有源对象的属性和值复制到目标对象中。 具体使用方法如下: ``` const target = {a: 1, b: 2}; const source = {b: 3, c: 4}; const merged = Object.assign(target, source); console.log(merged); // 输出:{a: 1, b: 3, c: 4} console.log(target); // 输出:{a: 1, b: 3, c: 4} console.log(source); // 输出:{b: 3, c: 4} ``` 在上面的例子中,目标对象是`target`,源对象是`source`。将`source`的属性和值复制到`target`中,通过Object.assign()方法实现对象的合并。合并后,`target`对象的b属性的值被覆盖为3,而其他属性保持不变。同时,返回的合并后的对象也是`target`对象。 需要注意的是,Object.assign()方法会修改目标对象本身,并且是浅拷贝,即只会复制对象的引用,而不会复制对象的子对象。如果源对象和目标对象的某个属性都是对象,那么合并后的结果中,这个属性会指向同一个对象。 如果只是想合并对象而不修改目标对象,可以先创建一个空对象作为目标对象,再合并源对象和目标对象。 另外,如果源对象中有相同属性名的属性,后面的源对象的属性值会覆盖前面的源对象的属性值。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值