在了解前端内存空间前,我们先学习三种基本数据结构:堆、栈、队列。
栈
栈是一种线性的数据结构,它遵循后进先出(LIFO)的原则。栈的特点是只能在栈顶进行插入和删除操作,因此栈的底部是栈中的最小值。
栈是自动分配相对固定大小的内存空间、并由系统自动释放
常见算法题应用
有效的括号、删除字符串中的所有相邻重复项…
堆
堆是一种树状的数据结构,每个节点都可以有零个或多个子节点。堆的特点是每个节点的值都大于或等于其子节点的值,因此堆的根节点是堆中的最大值。
堆是动态分配内存、内存大小不固定,也不自动释放,是一种无序的树状结构,满足key-value 键值对的存储方式
队列
队列是一种线性的数据结构,它遵循先进先出(FIFO)的原则。队列的特点是只能在队列的一端进行插入操作,在另一端进行删除操作,因此队列的头部是队列中的最小值。
队列可以在js的事件循环机制中,再深入理解TODO
js数据类型
基本类型
undefined 、null、number、string、boolean、symbol、bigint
值传递,栈内存中存储的是值的拷贝,所以对基本类型的操作不会影响到原变量。
const a = 10;
const b = a;
b = 20;
console.log(a); // 10
引用类型
Object、Array、Function、Date、等等
址传递,堆内存中存储的是地址的拷贝,对引用类型的操作会影响到原变量。
const a = { name: 'Kevin' };
const b = a;
b.name = 'Daisy';
console.log(a.name); // Daisy
深拷贝、浅拷贝
深拷贝
深拷贝是指完全复制一个对象,包括对象的所有属性和方法,以及对象的所有嵌套对象和嵌套数组。
深拷贝会创建一个新的对象,将原对象的所有属性和方法复制到新对象中,并且新对象的所有属性和方法都是新的对象,不会与原对象共享内存。
1、序列化和反序列
问题:undefined、function、symbol这三种类型的值是非安全的(包括该对象的属性循环赋值该对象),所以格式化后,就被过滤掉了,而set、map这种数据格式的对象,也并没有被正确处理,而是处理成了一个空对象。
另外,对于循环引用的对象,会直接报错
const a = { name: 'Kevin', age: 18 };
const b = JSON.parse(JSON.stringify(a));
b.name = 'Daisy';
console.log(a.name); // Kevin
// 循环引用
var data = {name: 'foo',child: null,
}
data.child = data
2、Object.assign
const a = { name: 'Kevin', age: 18 };
const b = Object.assign({}, a);
b.name = 'Daisy';
console.log(a.name); // Kevin
3、手写深拷贝
需要考虑对象、数组、处理循环引用
/** class类写法 */
class DeepClone {constructor(){cloneVal: null;}clone(val, map = new WeakMap()) {let type = this.getType(val); //当是引用类型的时候先拿到其确定的类型if (this.isObj(val)) {switch (type) {case 'date': //日期类型重新new一次传入之前的值,date实例化本身结果不变return new Date(val);break;case 'regexp': //正则类型直接new一个新的正则传入source和flags即可return new RegExp(val.source, val.flags);break;case 'function': //如果是函数类型就直接通过function包裹返回一个新的函数,并且改变this指向return new RegExp(val.source, val.flags);break;default:this.cloneVal = Array.isArray(val) ? [] : {};if (map.has(val)) return map.get(val)map.set(val, this.cloneVal)for (let key in val) {if (val.hasOwnProperty(key)) { //判断是不是自身的keythis.cloneVal[key] = new DeepClone().clone(val[key], map);}}return this.cloneVal;}} else {return val; //当是基本数据类型的时候直接返回}}/** 判断是否是引用类型 */isObj(val) { return (typeof val == 'object' || typeof val == 'function') && val != null}/** 获取类型 */getType(data) { var s = Object.prototype.toString.call(data);return s.match(/\[object (.*?)\]/)[1].toLowerCase();};
}/** 测试 */
var a ={a:1,b:true,c:undefined,d:null,e:function(a,b){return a + b},f: /\W+/gi,time: new Date(),}
const deepClone = new DeepClone()
let b = deepClone.clone(a)
console.log(b)
4、lodash库
_.cloneDeep()
浅拷贝
浅拷贝是指只复制对象的一层属性,而不复制对象的嵌套属性。
const a = { name: 'Kevin', age: 18 };
const b = {...a };
b.name = 'Daisy';
console.log(a.name); // Kevin
垃圾回收机制
JavaScript中有自动垃圾回收机制,会通过标记清除的算法识别哪些变量对象不再使用,对其进行销毁。开发者也可在代码中手动设置变量值为null(a = null)进行标记清除,让其失去引用,以便下一次垃圾回收时进行有效回收。
堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
为什么会有栈内存和堆内存之分?
通常与垃圾回收机制有关。为了使程序运行时占用的内存最小。
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
引用计数垃圾收集
如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
但是当循环引用时,就会出现内存泄漏。
标记清除
垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
然后从各个根对象开始遍历,把不是垃圾的节点改成1
清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收