js的数据类型
基本类型:number
,string
,null
,undefined
,boolean
,ES6新增symbol
类型
引用类型:object
,function
等等,要注意的是,{}
对象和[]
数组都是object
类型
堆栈
栈 (Stack):
自动分配的内存空间,系统自动释放
堆 (Heap)
动态分配的内存空间,大小不定,也不会自动释放
基本类型
基本类型的数值是直接存放在栈中的,可以直接访问。基本类型对变量的赋值是值传递,并不会改变原始值。
let str = 'abc'
str[0] = 'd'
console.log(str)
// abc
// 通过字符串索引来更改字符串并不会像我们期望的那样改变其原始值
str = 'cbc'
console.log(str)
// cbc
// 只有重新赋值才会改变变量str的值,但是依然无法改变'abc'的原始值
str_1 = str
// 将变量str的值传递给变量str_1
如图,执行
let str = 'abc'
时,会在栈底分配一块空间,存入值'abc',当重新给变量str
赋值时str = 'cbc'
,会分配另一块空间存入值'cbc',并将str
指向新分配的这块空间。由于基本类型原始值不可边,存储'abc'的栈内存并不会立即删掉,并留给系统等待系统自动处理。
当我们将str的值赋值给str_1时str_1 = str
,在栈中再开辟一块空间,将此时str
的值copy过来,并且将变量str_1
指向这块空间。
基本数据类型的比较是值的比较,只要两个变量的值是相等的,就认为他们是相等的。
let a = 1
let b = 1
console.log(a === b)
// true
引用类型
引用类型的存储方式相对于基本类型是复杂了一些的。其数据被存储在堆内存中,变量只是存放在占内存中的指针,指针指向堆内存中的地址。例如。
let arr_1 = [1, 2, 3]
let student = {name: 'inathan'}
let arr_2 = [1, 2, 3]
let student_1 = student
引用类型的比较是引用的比较,即比较栈中存放的地址是否相等。
console.log(arr_1 == arr_2)
// false
引用类型的值是可以直接改变的:
arr_1[0] = 4
console.log(arr_1)
// [4, 2, 3]
引用类型的赋值仅仅是将栈中存放的地址进行传递,student
赋值给student_1
,只是将student
变量的地址传递给了student_1
,此时改变student_1
的值,student
也会跟着一起改变。
student_1.name = 'wang'
console.log(student.name)
// wang
// 两个变量都指向同一地址,改变其中任何一个变量的值,都会改变另一个变量
题外话,es6中的
const
是定义一个不可变的变量,但是const
定义一个对象时,仍然可以改变其值,也是因为上面的原因,改变的是堆中的数据,并没有改变栈中的地址,没有违背const
的定义。
由此,可引出浅拷贝和深拷贝的概念。
浅拷贝
由于引用类型的变量直接相互赋值,无法真正的在堆中创建新数据,所以我们可以写一个方法,对数据进拷贝。该方法将创建一个空对象,并且遍历原对象,将元素一一复制到新对象里。
function copy (src) {
// 这里简单写一个只针对对象的浅拷贝,其他类型抛出异常
if (typeof src !== 'object') {
throw new Error('类型错误:没有对象!')
}
// 判断传入参数是数组还是object
let result = src.constructor.name === 'Array' ? [] : {}
// 遍历对象
for (let key in src) {
// 使用src.hasOwnProperty()方法过滤掉src对象中继承在原型链上的属性
if (src.hasOwnProperty(key)) {
result[key] = src[key] // 赋值
}
}
return result
}
let obj_1 = {
'name' : 'wang',
'age' : 29,
'arr' : [1, 2, [3, 4]],
}
let obj_2 = copy(obj_1)
obj_2.name = 'inathan'
obj_2.arr[0] = 5
console.log(obj_1)
//{
// 'name' : 'wang',
// 'age' : 29,
// 'arr' : [5, 2, [3, 4]],
//}
console.log(obj_2)
//{
// 'name' : 'inathan',
// 'age' : 29,
// 'arr' : [5, 2, [3, 4]],
//}
以上定义的copy()
方法只遍历的一层数据,当对象内含有子对象时,仍然只是将栈中的地址拷贝了过去,没有重新创建新的对象。
es6中的 ...
扩展运算符也是浅拷贝
深拷贝
for...in
所以,如果想完全拷贝一份新对象,可以利用递归的方法进行深拷贝。
只有for...in
方法才会遍历原型链上的属性,其他方法都会丢失。可以使用Object.hasOwnProperty()方法过滤掉
function deepCopy (src) {
// 这里简单写一个只针对对象的浅拷贝,其他类型抛出异常
if (typeof src !== 'object') {
throw new Error('类型错误:没有对象!')
}
// 判断传入参数是数组还是object
let result = src.constructor.name === 'Array' ? [] : {}
// 遍历对象
for (let key in src) {
// 使用src.hasOwnProperty()方法过滤掉src对象中继承在原型链上的属性
if (src.hasOwnProperty(key)) {
// 如果src子元素是对象,执行递归操作
if (src[key] && typeof src[key] === 'object') {
result[key] = deepCopy(src[key])
} else {
// 不是对象,直接赋值
result[key] = src[key]
}
}
}
return result
}
let obj_1 = {
'name' : 'wang',
'age' : 29,
'arr' : [1, 2, [3, 4]],
}
let obj_2 = deepCopy(obj_1)
obj_2.name = 'inathan'
obj_2.arr[0] = 5
obj_2.arr[2][0] = 6
console.log(obj_1)
//{
// 'name' : 'wang',
// 'age' : 29,
// 'arr' : [1, 2, [3, 4]],
//}
console.log(obj_2)
//{
// 'name' : 'inathan',
// 'age' : 29,
// 'arr' : [5, 2, [6, 4]],
//}
深拷贝有很多种方法,要考虑的引用类型也很多,也很复杂。下面收集一些方法。
序列化反序列化法:
function deepCopy (src) {
return JSON.parse(JSON.stringify(src))
}
这个方法不能复制函数,且原型链也会丢失(同上面的deepCopy函数,过滤了原型链上的对象):
function foo () {
this.name = 'wang'
}
foo.prototype.age = 20
let o = new foo()
o_1 = JSON.parse(JSON.stringify(o))
console.log(o_1.age)
// undefined
Reflect对象
静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。
function deepCopy (src) {
if (typeof src !== 'object') {
throw new Error('类型错误:没有对象!')
}
// 判断传入参数是数组还是object,进行浅拷贝
let result = src.constructor.name === 'Array' ? [...src] : {...src}
// 遍历浅拷贝后key组成的数组
Reflect.ownKeys(result).forEach(key => {
// 判断浅拷贝后的result子元素是否为object类型,如果是,需要再次拷贝,不是无需处理
if (typeof result[key] === 'object') {
result[key] = deepCopy(result[key]) // 子元素是对象,递归操作
}
})
return result
}