参考与:js中的栈、堆、队列、内存空间
1、深浅拷贝
将一个变量的值赋值给另一个变量,相当于在栈内存中创建了一个新的内存空间,然后从栈中复制值,存储到这个新空间中。对于基本类型,栈中存储的就是它自身的值,所以新内存空间存储的也是一个值。直接改变新变量的值,不会影响到旧变量的值,因为他们值存储的内存空间不同。
// 基本类型复制变量
let a = 10
let b = a
b = 20
console.log(a,b)//10 20
而对于引用类型来说,同样是复制栈中存储的值。但是栈存储的只是其引用地址,其具体的值存储在堆中。变量复制仅复制栈中存储的引用,不会复制堆中存储的值,所以新变量在栈中的值是一个地址指针。
// 引用类型复制变量
let a = [1,2]
let b = a //此时a b指向同一堆内存
b.push(5)
console.log(a,b)//[1,2,5] [1,2,5]
console.log(a == b)//true
可见,变量复制赋值,都属于栈存储拷贝,因此深浅拷贝可以这样区分:
- 浅拷贝:栈存储拷贝
- 深拷贝:栈、堆存储拷贝
深拷贝会同时开辟新的栈内存,堆内存空间。
目前实现深拷贝的方法不多,主要是两种:
①利用 JSON 对象中的 parse 和 stringify
②利用递归来实现每一层都重新创建对象并赋值(详见下面俩个链接)
// 利用JSON对象方法实现深拷贝
let a = [1,2]
let b = JSON.parse(JSON.stringify(a))
b.push(5)
console.log(a,b)//[1,2] [1,2,5]
console.log(a == b)//false
此方法只能适用于一些简单的情况,像如果要拷贝的对象中有function则不行,undefined、
function、symbol 会在转换过程中被忽略... 如下:
let obj = {
n:2,
f:function(){
console.log(1)
}
}
let x = JSON.parse(JSON.stringify(obj))
console.log(obj,x)//{n: 2, f: ƒ()} {n: 2}
console.log(obj == x)//false
函数传参是按值传递还是按引用传递?
let person = {age: 17}
function foo(person){
person.age = 23
}
foo(person)
console.log(person.age)
函数调用时,会对参数赋值。而参数传递过程其实同样是变量复制的过程,所以它是按值传递。var person = person,因为传递参数是对象时,变量复制仅复制的栈存储(浅拷贝),所以修改对象属性会造成外部变量对象的修改。
深浅拷贝更多更详细内容见:
JavaScript基础心法 深浅拷贝(浅拷贝和深拷贝)
js 深浅拷贝 笔记总结
2、js内存机制
1)、js中内存的生命周期
- 内存分配:当我们声明变量的时候,系统会自动为变量对象分配需要的内存
- 内存使用:在分配到的内存中进行读/写操作
- 内存回收:不再使用时将其销毁,释放内存
内存泄漏:不用的变量其内存未能及时释放。
2)、垃圾回收机制
js中有自动垃圾回收机制,会通过标记清除的算法识别哪些变量对象不再使用,对其进行销毁。开发者也可在代码中手动设置变量值为null(a = null)进行标记清除,让其失去引用,以便下一次垃圾回收时进行有效回收。
局部环境中,函数执行完成后,函数局部环境声明的变量不再需要时,就会被垃圾回收销毁(理想的情况下,闭包会阻止这一过程)。
全局环境下的变量只有页面退出时其生命周期才会结束。所以开发者应尽量避免在全局环境中创建全局变量,如需使用,也要在不需要时手动标记清除,将其内存释放掉。
详细的垃圾回收机制见:
JavaScript 内存机制
js内存管理机制
3)、为什么会有栈内存和堆内存之分?
通常与垃圾回收机制有关。为了使程序运行时占用的内存最小。
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。