在JS中复制对象

原文地址:Copying objects in Javascript, 原文作者:Victor Parmar
渣翻译,有英文阅读能力的可以去原网址阅读,正文部分的括号内是译者的尝试补充说明
自豪地采用谷歌翻译

在这篇文章中,我将会讲到几种在JS中复制对象的方式,我们将会关注到浅复制和深复制。
在开始之前,值得一提的是一些基础知识:JS中的对象只是对内存中某个位置的引用。这些引用是可以更改的。即他们可以被重新分配。从而,简单的复制引用的操作在结果上仅仅是将两个引用指向了内存中的同一位置。

var foo = {
  a: 'abc'
}
console.log(foo.a) // abc

var bar = foo
console.log(bar.a) // abc

foo.a = 'yo foo'
console.log(foo.a) // yo foo
console.log(bar.a) // yo foo

bar.a = 'whatup bar?'
console.log(foo.a) // whatup bar?
console.log(bar.a) // whatup bar?

正如你从上面例子中看到的,foo和bar都反映了同一个对象的变化。从而,在JS中复制对象需要小心,具体取决于您的用例。

浅复制

如果你的对象的属性的类型仅仅只是值类型(译者注:基本类型)的。你可以使用扩展运算符语法或者Object.assign(...)

var obj = { foo: 'foo', bar: 'bar' }
var copy = { ...obj } // Object { foo: 'foo', bar: 'bar' }
var obj = { foo: 'foo', bar: 'bar' }
var copy = Object.assign({}, obj) // Object { foo: 'foo', bar: 'bar' }

请注意,上述两种方法都可用于将属性值从多个源对象复制到目标对象:

var obj1 = { foo: 'foo' }
var obj2 = { bar: 'bar' }

var copySpread = { ...obj1, ...obj2 } // Object { foo: 'foo', bar: 'bar' }
var copyAssign = Object.assign({}, obj1, obj2) // Object { foo: 'foo', bar: 'bar' }

事实上,上述方法的问题在于对象的属性如果是一个对象,则只会复制该属性对象在内存中的引用。即它相当于var bar = foo,如同第一个代码例子:

var foo = { a: 0, b: { c: 0 } }
var copy = { ...foo }

copy.a = 1
copyb.c. = 2

console.dir(foo) // { a: 0, b: { c: 2 } }
console.dir(copy) // { a: 0, b: { c: 2 } }

深复制(有缺陷)

为了对对象进行深复制操作,一个潜在的解决方案是序列化对象为一个字符串,然后反序列化,生成一个新对象:

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

不幸的是,这个方法仅仅适用于当源对象包含可序列化的值类型并且没有循环引用的情况。不能序列化的值的类型,比如Date对象,即使它在字符串化上以ISO格式打印。JSON.parse仅仅会将它解释为一个字符串,而不是Date对象。

深复制(更少的缺陷)

对于更复杂的对象,可以使用更新的HTML5structured clone克隆算法。不幸的是,在撰写本文时,它仍局限于某些内置类型,但它支持的内容类型比JSON.parse更多。比如:DateRegExpMapSetBlobFileListImageData、稀疏和类型化数组。它还在克隆对象中保留了引用关系。允许它支持不适用于上述序列化方法的循环和递归结构。

当前还没有直接调用结构化克隆算法的方法,但一些新的浏览器特性可以被用来间接使用这个方法。从而,我们会得到一些可能用于深度复制对象的变通方法

使用 MessageChannel:这背后的想法是利用MessageChannel通信功能使用的序列化算法。这个功能是基于事件的,因此获取克隆结果是一个异步的操作。

class StructruedCloner {
  constructor() {
    this.pendingClones_ = new Map()
    this.nextKey_ = 0
    
    const channel = new MessageChannel()
    this.inPort_ = channle.port1
    this.outPort_ = channel.port2
    
    this.outPort_.onmessage = ({data: {key, value}}) => {
      const resolve = this.pendingClones_.get(key)
      resolve(value)
      this.pendingClones_.delete(key)
    }
    this.outPort_.start()
  }
  
  cloneAsync(value) {
    return new Promise(resolve => {
      const key = this.nextKey_++
      this.pendingClones_.set(key, resolve)
      this.inPort_.postMessage({key, value})
    })
  }
}

const structuredCloneAsync = window.structuredCloneAsync = 
      StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner)

const main = async () => {
  const original = { date: new Date(), number: Math.random() } // 译者注释:添加一些JSON方法不能解释的对象
  original.self = original // 译者注释:添加循环引用
  
  const clone = await structuredCloneAsync(original)
  
  // 不同的对象
  console.assert(original !== clone)
  console.assert(original.date !== clone.date)
  
  // 循环
  console.assert(original.self === original)
  console.assert(clone.self === clone)
  
  // 等价值
  console.assert(original.number === clone.number)
  console.assert(Number(original.date) === Number(clone.date))
  
  console.log('Assertions complete.')
}

main()

使用historyAPIhistory.pushState()history.replaceState()两个方法会创建它们第一个参数的结构化对象。注意这个方法是同步的,操纵浏览器历史记录不是一个快速的操作并且反复调用此方法可能导致浏览器无响应。

const structureClone = obj => {
  const oldState = history.state
  history.replaceState(obj.null)
  const clonedObj = history.state
  history.replaceState(oldState, null)
  return clonedObj
}

使用notificationAPI:当创建一个新的提醒(译者注:notification),构造函数会从它所关联的数据中创建一份结构化的克隆副本。注意,这么做浏览器会尝试将提醒显示给用户。但是这将会静默失败。除非应用已经请求到显示提醒的权限。万一权限存在,提醒会立即关闭。

const structuredClone = obj => {
  const n = new Notification('', {data: obj, silent: true})
  n.onshow = n.close.bind(n)
  return n.data
}

在NodeJS中进行深复制

在NodeJS的8.0.0版本中,它提供了一个序列化的API,它是兼容结构化克隆的。注意:这个API在本文撰写时(译者注:原文发表于2018.11.1)还是标记为实验性的:

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

对于版本低于8.0.0或者更稳定的实现,一种方法是:可以使用lodashcloneDeep方法。该方法也基于结构化克隆算法

结论

总而言之,在JS中复制对象最佳的算法是严重依赖于你所复制对象的上下文和类型的。而lodash是通用深复制函数最安全的选择。也许你会给出更高效的实现呢。下面是一个对Date对象也起效的深复制的函数:

function deepClone(obj) {
  var copy
  
  // 处理3种基础类型,和null、undefined
  if (obj === null || typeof obj !== 'object') return obj
  
  // 处理日期
  if (obj instanceof Date) {
    copy = new Date()
    copy.setTime(obj.getTime())
    return copy
  }
  
  // 处理数组
  if (Array instanceof Array) {
    copy = []
    for (var i = 0, len = obj.length; i < len; i++) {
      copy[i] = deepClone(obj[i])
    }
    return copy
  }
  
  // 处理函数
  if (obj instanceof Function) {
    copy = function() {
      return obj.apply(this, arguemnts)
    }
    return copy
  }
  
  // 处理对象
  if (obj instance Object) {
    copy = {}
    for (var attr in obj) {
      if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr])
    }
    return copy
  }
  
  throw new Error("Unable to copy obj as type isn't suported" + obj.constructor.name)
}

就个人而言,我期待能够在任何地方使用结构化克隆,最后让这个问题得到休息,快乐的克隆:)

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值