从浅拷贝与深拷贝发现 JSON.stringify 的 “魅力”

1,278 阅读4分钟

基本类型

基本数据类型有7种:NumberStringBooleanNullUndefinedSymbol(ES6)BigInt(ES10)。变量均按值存放于栈中,赋值直接用=即可。

引用类型

引用数据类型有1种:Object。变量内存地址存放于栈中,值存在堆中,引用类型的赋值与下面讨论的浅拷贝深拷贝密切相关。

浅拷贝

首先声明 浅拷贝 ≠ 赋值

赋值=赋的是对象的内存地址,两个对象指向堆中同一份存储空间,互相影响。

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML']
}
let obj2 = obj1
obj2.name = '七金'
obj2.list[0] = 'Java'
// {name: '七金', list: ['Java', 'CSS', 'HTML']} 
// {name: '七金', list: ['Java', 'CSS', 'HTML']} 

浅拷贝是在堆中创建新的内存空间,拷贝后对象的基本数据类型互不影响,但引用类型依然共享同一份存储空间,会互相影响。

function shallowClone (obj1) {
  let obj2 = {}
  for (let i in obj1) { 
    obj2[i] = obj1[i] 
  } 
  return obj2;
}
let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML']
}
let obj2 = shallowClone(obj1)
obj2.name = '七金'
obj2.list[0] = 'Java'
console.log(obj1, obj2)
// {name: '瑾行', list: ['Java', 'CSS', 'HTML']} 
// {name: '七金', list: ['Java', 'CSS', 'HTML']} 

Array

整理可数组浅拷贝相关api

扩展运算符

let arr1 = [1,[2],3]
let arr2 = [...arr1]
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2,5],3] [4,[2,5],3]

Array.prototype.slice

let arr1 = [1,[2],3]
let arr2 = arr1.slice()
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2,5],3] [4,[2,5],3]

Array.prototype.concat

let arr1 = [1,[2],3]
let arr2 = arr1.concat([])
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2,5],3] [4,[2,5],3]

Array.from

将类数组或可迭代对象创建一个新的浅拷贝数组实例。

let arr1 = [1,[2],3]
let arr2 = Array.from(arr1)
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2,5],3] [4,[2,5],3]

Array.prototype.map

let arr1 = [1,[2],3]
let arr2 = arr1.map(item => item)
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2,5],3] [4,[2,5],3]

Array.prototype.filter

let arr1 = [1,[2],3]
let arr2 = arr1.filter(item => item)
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2,5],3] [4,[2,5],3]

Array.prototype.reduce

reduce这里可能有点滥竽充数🤣,没有真正体现它的价值,算是提供一种新奇的思路吧。

let arr = [1,[2],3]
let arr2 = arr.reduce((arr1,item) => {
  arr1.push(item)
  return arr1
}, [])
arr2[0] = 4
arr2[1].push(5)
console.log(arr, arr2) // [1,[2,5],3] [4,[2,5],3]

Object

整理可对象浅拷贝相关api

扩展运算符

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML']
}
let obj2 = {...obj1}
obj2.name = '七金'
obj2.list[0] = 'Java'
console.log(obj1, obj2) 
// {name: '瑾行', list: ['Java', 'CSS', 'HTML']} 
// {name: '七金', list: ['Java', 'CSS', 'HTML']} 

Object.assign

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML']
}
let obj2 = Object.assign({}, obj1)
obj2.name = '七金'
obj2.list[0] = 'Java'
console.log(obj1, obj2) 
// {name: '瑾行', list: ['Java', 'CSS', 'HTML']} 
// {name: '七金', list: ['Java', 'CSS', 'HTML']} 

深拷贝

堆内存重新开辟全新的内存存放新对象,两个对象不会互相影响。

Array

序列化

利用JSON.stringify将数组转为JSON字符串,再用JSON.parse将字符串转为新数组。

let arr1 = [1,[2],3]
let arr2 = JSON.parse(JSON.stringify(arr1))
arr2[0] = 4
arr2[1].push(5)
console.log(arr1, arr2) // [1,[2],3] [4,[2,5],3]

Object

序列化

利用JSON.stringify将对象转为JSON字符串,再用JSON.parse将字符串转为新对象,但这个方法存在弊端。

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML']
}
let obj2 = JSON.parse(JSON.stringify(obj1))
obj2.name = '七金'
obj2.list[0] = 'Java'
console.log(obj1, obj2)
// {name: '瑾行', list: ['JS', 'CSS', 'HTML']} 
// {name: '七金', list: ['Java', 'CSS', 'HTML']} 

貌似看起来没有任何问题,也不用引入lodash库。那么,现在给对象添加个方法看看~

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML'],
  work: function() {}
}
let obj2 = JSON.parse(JSON.stringify(obj1))
console.log(obj1, obj2)
// {name: '瑾行', list: ['JS', 'CSS', 'HTML'], work: function() {}} 
// {name: '瑾行', list: ['JS', 'CSS', 'HTML']} 

方法在JSON.stringify后丢失了... 万事总有解决办法,实在不行引lodash库。

我当时处理方法是将函数转为字符串确保不再丢失,最后再利用new Function()去将字符串转为函数。

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML'],
  work: function() {},
}
for(let i in obj1) {
  if(typeof obj1[i] === 'function') {
    obj1[i] = obj1[i].toString()
  }  
}
let obj2 = JSON.parse(JSON.stringify(obj1))
for(let i in obj2) {
  if(typeof obj2[i] === 'string' && obj2[i].indexOf('function') === 0) {
    obj2[i] = new Function('return ' + obj2[i])
  }   
}
console.log(obj1, obj2)
// {name: '瑾行', list: ['JS', 'CSS', 'HTML'], work: function() {}} 
// {name: '瑾行', list: ['JS', 'CSS', 'HTML'], work: function() {}} 

当然这个方法也并不是完美的,比如我确实有个字段为string类型,且值就是function,那就真是凑巧了。

下载.jfif

这个现象引发了我对JSON.stringify的兴趣,还会丢失哪些类型的数据?列举了写属性...

let obj1 = {
  name: '瑾行',
  list: ['JS','CSS', 'HTML'],
  work: function() {},
  date: new Date(),
  reg: new RegExp(),
  symbol: Symbol(),
  number: NaN,
  address: Infinity,
  age: undefined
}
console.log(JSON.stringify(obj1))
 
// {
//  "name":"瑾行",
//  "list":["JS","CSS","HTML"],
//  "date":"2021-08-28T11:43:33.545Z",
//  "reg":{},
//  "number":null,
//  "address":null
//  }

发现真的是深坑🤦‍♀️

  1. 函数、Symbolundefined丢失
  2. NaNInfinitynull
  3. RegExp对象变{}
  4. Date对象转为字符串

继续在网上找JSON.stringify踩坑文章,以下情况也得小心

  1. 方法自带toJSON, 直接返回函数return
let obj1 = {
  name: '瑾行',
  toJSON: function() {
    return '瑾行'
  }
}
console.log(JSON.stringify(obj1)) // 瑾行
  1. 属性引用自身,会报错
let obj1 = {
  name: '瑾行',
  copy: obj1
}
console.log(JSON.stringify(obj1)) 

image.png

  1. 存在不可枚举属性,也会丢失
let obj1 = {
  name:  '瑾行'
}
Object.defineProperty(obj1, 'name', {
  enumerable: false
})
console.log(JSON.stringify(obj1)) // {}

这些情况,有一些同样可以先转字符串,再转回原属性类型,算是一种思路吧,但都在对象value情况可知的大前提下,不然还是用成熟的lodash中的cloneDeep吧。

src=http___img.doutula.com_production_uploads_image_2018_05_18_20180518604091_DMZqeL.jpg&refer=http___img.doutula.jfif

深拷贝这么复杂,准备之后研究lodash中的cloneDeep源码,手写试试看,先起个草稿。
如果大家觉得有帮助,欢迎 点赞 + 关注 + 收藏🧡,交流学习