赋值、浅拷贝及深拷贝

赋值、浅拷贝及深拷贝

一、数据类型

JavaScript的类型分为两⼤类,⼀类是原始(基本)类型,⼀类是复杂(引⽤)类型。

基本类型:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

复杂类型:

  • Object

还有⼀个没有正式发布但即将被加⼊标准的原始类型BigInt。

顺便提一提为什么会有BigInt的提案:JavaScriptNumber.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。 但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。

二、JavaScript的基本类型和复杂类型是储存在哪⾥的

基本类型储存在栈中,但是⼀旦被闭包引⽤则成为常住内存,会储存在内存堆中。

复杂类型会储存在内存堆中。 (存储的是该对象在栈中的引用,真实的数据存放在堆内存里)

三、赋值

3.1、基本类型赋值

变量中如果存储的是简单类型的数据,那么变量中存储的是值本身,如果将变量赋值给另一个变量,是将内部的值复制一份给了另一个变量,两个变量之间没有联系,一个变化,另一个不会同时变化。

// 基础数据类型
var a = 5 
var b = a // 将 a 内部储存的数据 5 复制了一份给 b
a = 10
console.log(a) // 10
console.log(b) // 5
3.2、复杂类型赋值

赋值操作(包括对象作为参数、返回值),不会开辟新的内存空间,他只是赋值了对象的引用,即该对象在栈的地址,而不是在堆中的数据。也就是说,将一个对象赋值给另一个对象时,两个对象指向的是同一个存储空间,无论哪个对象发生改变,都会通过存储空间找到堆中的数据将其改变,这样另一个对象也会改变。

// 复杂数据类型(对象和数组同理)
var p = {name: "tangsan", age: 18, sex: true}
var p1 = p // p 将内部储存的栈的地址赋值给了 p1
// 两个变量之间是一个联动的关系,一个变化,会引起另一个变化
p.name = "xiaowu"
console.log(p) // { age: 18, name: "xiaowu", sex: true }
console.log(p1) // { age: 18, name: "xiaowu", sex: true }

// 数组和函数存储在变量中是,也是储存的地址
var arr = [1, 2, 3, 4]
var arr2 = arr
arr[4] = 5
console.log(arr) // [1, 2, 3, 4, 5]
console.log(arr2) // [1, 2, 3, 4, 5]

四、浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

  • Obeject.assign()
var obj = { a: {name: "tangsan", age: 39} }
var obj1 = Object.assign({}, obj)
obj1.a.name = "xiaowu"
console.log(obj.a.name) // xiaowu
// 注意⚠️ 当对象只有一层的时候Obeject.assign()是深拷贝
let obj = {
   name: 'tangsan'
}
let obj2 = Object.assign({},obj)
obj2.name = 'xiaowu'
console.log(obj)  //{name: "tangsan"}

// 或者 当修改对象的第一层的时候是深拷贝
var obj = { a: {name: "tangsan", age: 39} }
var obj1 = Object.assign({}, obj)
obj1.a = { name: "xiaowu" }
// 修改的是obj的第一层,所以不会改变原对象
console.log(obj.a) // {name: "tangsan", age: 39}

总结: 只要是修改对象的第一层数据,那么Obeject.assign()是深拷贝

  • Arrany.prototype.concat()
let arr = [1, 3, {
   name: 'tangsan'
}]
let arr2 = arr.concat()
arr2[2].name = 'xiaowu'
console.log(arr) // [1, 3, { name: 'xiaowu' }]

let arr = [1, 3, [4, 5]]
let arr2 = arr.concat()
arr2[2][0] = 6
console.log(arr) // [1, 3, [6, 5]]
// 注意⚠️ 同上,只要是修改第一层的数据,Arrany.prototype.concat()是深拷贝
let arr = [1, 3, {
   name: 'tangsan'
}]
let arr2 = arr.concat()
arr2[1] = 4
console.log(arr) // [1, 4, { name: 'tangsan' }]
  • Arrany.prototype.slice()
let arr = [1, 3, {
   name: 'tangsan'
}]
let arr2 = arr.slice()
arr2[2].name = 'xiaowu'
console.log(arr) // [1, 3, { name: 'xiaowu' }]

let arr = [1, 3, [4, 5]]
let arr2 = arr.slice()
arr2[2][0] = 6
console.log(arr) // [1, 3, [6, 5]]
// 注意⚠️ 同上,只要是修改第一层的数据,Arrany.prototype.slice()是深拷贝
let arr = [1, 3, {
   name: 'tangsan'
}]
let arr2 = arr.slice()
arr2[1] = 4
console.log(arr) // [1, 4, { name: 'tangsan' }]

补充: Arrayconcatslice方法不会修改原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。

浅拷贝总结:针对concatslice只修改第一层的数据,它们将是深拷贝的说法: 通俗易懂的来说,当使用concatslice对数组进行浅拷贝的时候,相当于是对数组做了一个循环,会判断数组中每个元素的类型:

  • 如果这个元素的类型是基本类型,在新的数组中修改这个元素的值将不会原数组的值,也就是所说的修改第一层的数据时,它是深拷贝。
  • 如果这个元素的类型是基本类型,在新的数组中修改这个元素的值就原数组的值,即浅拷贝。

例如上述案例中其一:

let arr = [1, 3, [4, 5]]
let arr2 = arr.slice()
arr2[2][0] = 6  // arr2[2]这个元素是复杂类型的值,所以修改arr2[2]元素的值,是浅拷贝,会改变原数组
arr2[1] = 8   // arr2[1]这个元素是基本类型的值,所以修改arr2[1]元素的值,是深拷贝,不会改变原数组
console.log(arr) // 故而最后的结果是这样的: [1, 3, [6, 5]]

五、深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

  • JSON.parse(JSON.stringify())
let arr = [1, 3, {
   name: 'tangsan'
}]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].name = 'xiaowu'
console.log(arr) // [1, 3, { name: 'tangsan' }]

这种方法虽然可以对数组或对象进行深拷贝,但是不能拷贝函数。如下图的test属性为undefined的会丢失,函数也存在一定的问题。

let arr = [1, 3, {
   name: 'tangsan',
   test: undefined
}, function(){}]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].name = 'xiaowu'
console.log(arr) // [1, 3, { name: 'tangsan' }, null]
console.log(arr2 ) // [1, 3, { name: 'tangsan' }, null]
  • 封装递归方法实现深度克隆
/*** deep clone 
 * @param {[type]} parent object 需要进⾏克隆的对象 
 * @return {[type]} 深克隆后的对象 
 */ 
const clone = parent => { 
  // 判断类型 
  const isType = (obj, type) => { 
    if (typeof obj !== "object") return false 
    const typeString = Object.prototype.toString.call(obj) 
    let flag 
    switch (type) { 
      case "Array":
        flag = typeString === "[object Array]" 
        break 
      case "Date": flag = typeString === "[object Date]" 
        break 
      case "RegExp": flag = typeString === "[object RegExp]" 
        break 
      default: flag = false 
    }
    return flag 
  }
  
  // 处理正则 
  const getRegExp = re => { 
    var flags = "" 
    if (re.global) flags += "g" 
    if (re.ignoreCase) flags += "i" 
    if (re.multiline) flags += "m" 
    return flags 
  }
  
  // 维护两个储存循环引⽤的数组 
  const parents = [] 
  const children = []
  
  const _clone = parent => { 
    if (parent === null) return null 
    if (typeof parent !== "object") return parent 
    let child, proto 
    if (isType(parent, "Array")) { 
      // 对数组做特殊处理 
      child = [] 
    } else if (isType(parent, "RegExp")) { 
      // 对正则对象做特殊处理 
      child = new RegExp(parent.source, getRegExp(parent)) 
      if (parent.lastIndex) child.lastIndex = parent.lastIndex 
    } else if (isType(parent, "Date")) { 
      // 对Date对象做特殊处理 
      child = new Date(parent.getTime()) 
    } else { 
      // 处理对象原型 
      proto = Object.getPrototypeOf(parent) 
      // 利⽤Object.create切断原型链 
      child = Object.create(proto) 
    }
    // 处理循环引⽤ 
    const index = parents.indexOf(parent) 
    if (index != -1) { 
      // 如果⽗数组存在本对象,说明之前已经被引⽤过,直接返回此对象 
      return children[index] 
    }
    parents.push(parent) 
    children.push(child) 
    for (let i in parent) { 
      // 递归 child[i] = _clone(parent[i]) 
    }
    return child 
  }
  return _clone(parent)
}
  • 函数库lodash
var _ = require('lodash')
var obj1 = {
   a: 1,
   b: { f: { g: 1 } },
   c: [1, 2, 3]
}
var obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f) // false
  • Immutable.js

Immutable.js是⾃成⼀体的⼀套数据结构,性能良好,但是需要学习额外的API 。

  • immer

利⽤Proxy特性,⽆需学习额外的api,性能良好 。

首先需要安装npm i --save immer,引入import produce from 'immer'

let currentState = {
  a: [],
  p: {
    x: 1
  }
}

let nextState = produce(currentState, (draft) => {
  draft.a.push(2)
})

currentState.a === nextState.a // false
currentState.p === nextState.p // true

或:

let producer = produce((draft) => {
  draft.x = 2
})
let nextState = producer(currentState)

immer对于使用react的有很大的帮助。假如有以下对象,它有多层数据:

this.state = {
    vo: {
        condition: {
            "name": null,
            "status": null
        },
        pageNum: 1,
        pageSize: 10
    },
}

我们在setState改变condition里面的某个值的时候,一般会如下操作:

formFinish = (values) => {
  this.setState({
      vo: {
          ...this.state.vo,
          condition: values
      }
  })
}
formFinish = (values) => {
  this.setState(produce(draft => {
    draft.vo.condition = values
  }))
}

六、总结

以下总结不包含赋值中的基本类型赋值,所说赋值为复杂类型赋值。

和原数据是否指向同一对象第一层数据为基本数据类型原数据中包含复杂类型
复杂类型赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变不会使原数据一同改变改变会使原数据一同改变
深拷贝改变不会使原数据一同改变改变不会使原数据一同改变
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值