赋值、浅拷贝及深拷贝
一、数据类型
JavaScript的类型分为两⼤类,⼀类是原始(基本)类型,⼀类是复杂(引⽤)类型。
基本类型:
- boolean
- null
- undefined
- number
- string
- symbol
复杂类型:
- Object
还有⼀个没有正式发布但即将被加⼊标准的原始类型BigInt。
顺便提一提为什么会有BigInt
的提案:JavaScript
中Number.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' }]
补充: Array
的concat
和slice
方法不会修改原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。
浅拷贝总结:针对concat
和slice
只修改第一层的数据,它们将是深拷贝的说法: 通俗易懂的来说,当使用concat
或slice
对数组进行浅拷贝的时候,相当于是对数组做了一个循环,会判断数组中每个元素的类型:
- 如果这个元素的类型是基本类型,在新的数组中修改这个元素的值将不会原数组的值,也就是所说的修改第一层的数据时,它是深拷贝。
- 如果这个元素的类型是基本类型,在新的数组中修改这个元素的值就会原数组的值,即浅拷贝。
例如上述案例中其一:
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
}))
}
六、总结
以下总结不包含赋值中的基本类型赋值,所说赋值为复杂类型赋值。
和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含复杂类型 | |
---|---|---|---|
复杂类型赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |