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 之深浅拷贝 #17

Open
zenglinan opened this issue Aug 28, 2019 · 0 comments
Open

JavaScript 之深浅拷贝 #17

zenglinan opened this issue Aug 28, 2019 · 0 comments

Comments

@zenglinan
Copy link
Owner

zenglinan commented Aug 28, 2019

对象拷贝是经常会遇到的场景, 本文会总结几种深浅拷贝的方法

一. 浅拷贝

1. 浅拷贝的定义

先来说说什么是浅拷贝, 首先要明确一点:

直接拷贝对象的引用这不叫浅拷贝

先来看一个案例:

拷贝引用:

var obj1 = {
  a: 1,
  b: {
    c: 1
  }
}

var obj2 = obj1  // 拷贝 obj1 的引用

obj2.a = 2
console.log(obj1.a)  // 2

浅拷贝:

var obj1 = {
  a: 1,
  b: {
    c: 1
  }
}

var obj2 = {...obj1}

obj2.a = 2
console.log(obj1.a)  // 1

上面可以看到, 直接复制引用时, 两个对象实际保存的是堆中同一个对象的引用, 而浅拷贝则不同, 浅拷贝重新生成了一个新的对象, 但是浅拷贝只会拷贝一层, 假如原对象的属性值为对象, 也只会拷贝这个对象的引用。

也就是说: 在浅拷贝的对象中, 修改基本类型的值不会影响原数据, 修改复杂类型时, 会影响原数据

2. 浅拷贝的实现

(1) ...扩展运算符

var copy = {...obj} 

ES6 中提供了扩展运算符, 可以遍历取出对象身上的属性, 分配到新的对象上

(2) Object.assign

var copy = {}
Object.assign(copy, obj)

Object.assign 可以将第二个对象里可枚举的属性复制到第一个对象里

(3) for...in

function shallowCopy(src){
  let result = {}
  for(let prop in src){
    if(src.hasOwnProperty(prop)){
      result[prop] = src[prop]
    }
  }
  return result
}

注意: for...in 循环会遍历原型链上所有的的属性, 浅拷贝只需拷贝对象身上的属性。

所以加一层 hasOwnProperty 判断

二. 深拷贝

1. 深拷贝的定义

与浅拷贝相比, 深拷贝也会对对象的子对象进行拷贝

当修改拷贝对象上的对象属性时, 不会对源对象产生影响

2. 深拷贝的实现

(1) JSON序列化与反序列化

最先说的办法当然是家喻户晓的 JSON 序列化与反序列化方法啦~

var copy = JSON.parse(JSON.stringify(src))

先序列化一下对象变成字符串, 然后再反序列化成对象

这个办法有两个缺陷:

  1. 既然是 JSON 对象上的方法, 当然不支持拷贝函数了o(╥﹏╥)o

  2. 同样的道理, JSON 里面没有 Symbol 类型, 自然也不支持 Symbol 类型的键

但是这个方法最简单了, 一行代码...

(2) for...in + 递归

function deepClone(src){
  if(typeof src !== "object") return src

  let result = Array.isArray(src) ? [] : {}

  for(let prop in src){
    if(!src.hasOwnProperty(prop)) continue
    let value = src[prop]
    result[prop] = typeof value === 'object' ? deepClone(value) : value
  }
  return result
}

这个方法比 JSON 序列化的方法更完善的一点是: 支持拷贝函数

但是依然无法拷贝键为 Symbol 类型的属性,因为 for...in...无法访问到 Symbol 类型的属性,并且依然存在循环引用问题

这里我们先解决循环引用的问题,先写一个循环引用:

var o = {
  a: 1
}
o.o = o

我们上面的代码之所以出现循环引用的问题是因为:

当第一次遍历我们要拷贝的 o 对象的时候,检测到 o 这个键的值也是对象,所以执行了 deep(o.o),但由于对象循环引用,在递归的过程中,o 键对应的值始终是对象,所以递归会一直进行下去,找不到递归出口。

要解决这个问题,办法也简单,只需要给循环引用提供一个递归出口。在执行拷贝之前先检查 cache 中是否存储了这个对象,如果没有存储,说明没有拷贝过,就将它存储起来,如果存储了就直接返回,这就给循环引用提供了递归出口

网上比较多用的是 Map,这里我们用数组写一下,思路是一样的,只需要可以存储拷贝过的对象即可。

function deepClone(obj, cache = []){
  if(typeof obj !== 'object') return obj
  const result = Array.isArray(obj) ? [] : {}
  if(cache.includes(obj)){  // 如果拷贝过直接返回
    return obj
  }else { // 没有拷贝过就存储起来
    cache.push(obj)
  }
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      const val = obj[key]
      //  注意每次要把 cache 传进去
      result[key] = typeof val === 'object' ? deepClone(val, cache) : val
    }
  }
  return result
}

好了,接下来我们来解决无法拷贝 Symbol 属性的问题

(3) Reflect.ownKeys() + 递归

MDN 上是这样描述的:

Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于

Object.getOwnPropertyNames(target)
.concat(Object.getOwnPropertySymbols(target))

这个方法可以取到对象自身的所有属性, 包括 Symbol 类型的属性

function deepClone(obj, cache = []){
  if(typeof obj !== 'object') return obj
  const result = Array.isArray(obj) ? [] : {}
  if(cache.includes(obj)){
    return obj
  }else {
    cache.push(obj)
  }
  Reflect.ownKeys(obj).forEach((key) => {
    const val = obj[key]
    result[key] = typeof val === 'object' ? deepClone(val, cache) : val
  })
  return result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant