数据存储
1. javaScript数据类型
1.1 共8种
前面的 7 种数据类型称为原始类型,把最后一个对象类型称为引用类型
2. 判断数据类型的几种方法
2.1 typeof(常用于判断基本数据类型,对于引用数据类型全部返回Object)
-
语法: typeof [检测数据] 返回数据的类型名称
-
特点
- 对于基本类型,除 null 以外,均可以返回正确的结果。
- 对于引用类型,除 function 以外,一律返回 object 类型。
- 对于 null ,返回 object 类型。
- 对于 function 返回 function 类型。
2.2 instanceof(检测某一个实例的原型链上是否有这个类的原型属性)
-
语法:[监测数据] instanceof [class] :返回true或false
-
特点:
- 可以区分复杂数据类型
- 只要在当前实例的原型链上,我们用其检测出来的结果都是true
- 基本类型值检测繁琐,且检测不全(undefined, null, symbol)
-
原理:验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,
instanceof
在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype
,找到返回true,未找到返回false -
手写instanceof
function myinstanceOf_(left, right) { let proto = left.__proto__; let prototype = right.prototype while (true) { if (proto == null) return false if (proto == prototype) return true proto = proto.__proto__; } }
2.3 constructor (用于引用数据类型)
- 语法: 检测数据.constructor === class
- 特点:
- 适合使用在引用数据类型上
- 原型链不会干扰
- 原理:构造函数原型上有一个 constructor 属性指向构造函数自身的,如果在实例上使用 construtor 时,就会直接使用其构造函数原型的上的该属性,并指向其构造函数。
2.4 Object.prototype.toString.call()(对象原型链判断方法)
- 语法:Object.prototype.toString.call(检测数据)
- 特点:适用于所有类型的判断检测
- 原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果
3. 内存空间
3.1 内存空间分配
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。
代码空间主要是存储可执行代码
3.2 栈和堆空间
function foo(){
var a = "闷倒驴";
var b = a;
var c = {name:"王美丽"};
var d = c;
}
foo()
为什么不能把存储在堆中的数据存储在栈中?
因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。
堆栈特点
-
栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间
-
原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址
3.3 数据的赋值与引用数据类型的深浅拷贝
赋值:简单数据类型直接在栈中开辟一块新的内存,存储赋值的数据;引用数据类型,在栈中开辟一块空间,存储赋值的数据对应的堆中的存储地址,源数据和拷贝的新数据对应的是同一块堆空间中的数据
浅拷贝:堆栈各开辟一块新空间,栈中存储堆中新开辟的空间的地址,堆中赋值了源数据的数据,如果值是基本数据类型那么新数据和源数据没有任何关系,如果值是引用数据类型那么新数据的值指向的源数据的数据存储地址
深拷贝:堆栈各开辟一块新空间,栈中存储堆中新开辟的空间的地址,堆中存储的数据和源数据一样,但是二者没有任何联系
赋值例子:
var user = {userName:'闷倒驴',sex:'女',body:{weight:'50kg',height:'160'}}
// 赋值
var userCopy = user;
user.sex = '男';
user.body.weight = '100斤';
浅拷贝例子:
var user = {userName:'闷倒驴',sex:'女',body:{weight:'50kg',height:'160'}}
// 浅拷贝
var userCopy = Object.assign({}, user);
user.sex = '男';
user.body.weight = '100斤';
当执行完userCopy浅拷贝之后,内存空间如图所示:
执行完 user.sex = ‘男’; user.body.weight = ‘100斤’; 之后,如下图所示
深拷贝例子:
var user = { userName: '闷倒驴', sex: '女', body: { weight: '50kg', height: '160' } }
// 深拷贝
var userCopy = JSON.parse(JSON.stringify(user));
user.sex = '男';
user.body.weight = '100斤';
当执行完userCopy深拷贝之后,内存空间如图所示:
当执行完userCopy深拷贝之后,内存空间如图所示:
浅拷贝方法:
1.Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2.函数库lodash的_.clone方法
该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
3.展开运算符…
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。
let obj1 = { name: '王美丽', address: { x: 100, y: 100 } }
let obj2 = { ...obj1 }
obj1.address.x = 200;
obj1.name = '闷倒驴'
console.log('obj2', obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
4.Array.prototype.concat()
let arr = [1, 3, {
username: '王美丽'
}];
let arr2 = arr.concat();
arr2[2].username = '闷倒驴';
console.log(arr); //[ 1, 3, { username: '闷倒驴' } ]
5.Array.prototype.slice()
let arr = [1, 3, {
username: ' 王美丽'
}];
let arr3 = arr.slice();
arr3[2].username = '闷倒驴'
console.log(arr); // [ 1, 3, { username: '闷倒驴' } ]
深拷贝方法:
1.JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: '闷倒驴'
}];
let arr1 = JSON.parse(JSON.stringify(arr));
arr1[2].username = '王美丽';
console.log(arr, arr1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S5tpaUg5-1666523089124)(img\07.png)]
这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。
比如下面的例子:
let arr = [1, 3, {
username: '闷倒驴'
},function(){}];
let arr1 = JSON.parse(JSON.stringify(arr));
arr1[2].username = '王美丽';
console.log(arr, arr1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8uwOenux-1666523089124)(img\06.png)]
2.函数库lodash的_.cloneDeep方法
该函数库也有提供_.cloneDeep用来做 Deep Copy
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
3.jQuery.extend()方法
jquery 有提供一個$.extend
可以用来做 Deep Copy
// $.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
4.手写递归方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝,遇到循环引用,将引用存储起来,如果存在就不再拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
循环引用:
当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用,(当然不止这一种情况,不过原理是一样的)
垃圾回收机制
1. 垃圾回收的方式
手动垃圾回收和自动垃圾回收
c语言是手动垃圾回收
javaScript是自动垃圾回收
2. javaScript垃圾回收的方式
- 调用栈中的垃圾数据回收方式
- 堆中垃圾数据的回收方式
3. 调用栈中的垃圾数据回收方式
当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文
function foo(){
var a = 1;
var b = {name:"王美丽"};
function showName(){
var c = 2;
var d = {name:"闷倒驴"};
};
showName()
};
foo()
4. 堆中的垃圾回收方式
当函数直接结束,栈空间处理完成了,但是堆空间的数据虽然没有被引用,但是还是存储在堆空间中,需要垃圾回收器将堆空间中的垃圾数据回收
4.1 代际假说
代际假说有以下两个特点:
-
第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
-
第二个是不死的对象,会活得更久。
4.2 分代收集
为了使垃圾回收达到更好的效果,根据对象的生命周期不一样,使用不同的垃圾回收的算法
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
副垃圾回收器,主要负责新生代的垃圾回收。
主垃圾回收器,主要负责老生代的垃圾回收。
4.3 垃圾回收器的工作流程
第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。
4.4 新生区垃圾回收(副垃圾回收器)
新生区特点:
-
通常把小的对象分配到新生区
-
新生区的垃圾回收比较频繁
-
通常存储容量在1-8M
-
Scavenge算法采用复制机制,如果存储容量过大,会导致每次清理的时间过长,效率低
-
同时因为存储容量小,很容易就写满,所以经过两次垃圾回收依然还存活的对象,会被移动到老生区中
这个策略称为对象晋升策略
-
-
新生代中用 Scavenge 算法来处理垃圾回收
Scavenge算法:将新生区分成两部分,一部分叫对象区域,一部分叫空闲区域,新加入的对象先存放在对象区域,当对象区域写满,进行垃圾回收,具体回收步骤
-
标记:对对象区域中的垃圾进行标记
-
清除垃圾数据和整理碎片化内存:副垃圾回收器会把这些存活的对象复制到空闲区域中,并且有序的排列起来,复制后空闲区域就没有内存碎片了
-
角色翻转:完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去
4.5 老生区垃圾回收(主垃圾回收器)
老生区的特点:
- 对象占用空间大
- 对象存活时间长
垃圾回收的过程: 之前使用标记-清除算法,由于会产生碎片化空间,于是又添加标记-整理算法
-
标记-清除算法
- 标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
- 清除:将垃圾数据进行清除。
- 碎片:
对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。
-
标记-整理算法(如图会产生大量的碎片化空间,没有连续的大存储空间,如果此时要存入一个大对象,就存储不了)
- 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象。
- 整理:让所有存活的对象都向内存的一端移动
- 清除:清理掉端边界以外的内存
4.6 全停顿
V8 是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法
5. 避免内存泄漏的方式
- 内存溢出:就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
- 内存泄漏:是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问,而系统也不能再次将它分配给需要的程序。就是产生了不可回收的垃圾数据
5.1 尽可能少地创建全局变量
在ES5中以var
声明的方式在全局作用域中创建一个变量时,或者在函数作用域中不以任何声明的方式创建一个变量时,都会无形地挂载到window
全局对象上,如下所示:
var num = 1; // 等价于 window.num = 1;
function fun() {
num = 1;
}
等价于
function fun() {
window.num = 1;
}
我们在fun
函数中创建了一个变量num
但是忘记使用var
来声明,此时会意想不到地创建一个全局变量并挂载到window对象上,另外还有一种比较隐蔽的方式来创建全局变量:
function fun() {
this.num = 1;
}
fun(); // 相当于 window.fun()
当foo
函数在调用时,它所指向的运行上下文环境为window
全局对象,因此函数中的this
指向的其实是window
,也就无意创建了一个全局变量。当进行垃圾回收时,在标记阶段因为window
对象可以作为根节点,在window
上挂载的属性均可以被访问到,并将其标记为活动的从而常驻内存,因此也就不会被垃圾回收,只有在整个进程退出时全局作用域才会被销毁。如果你遇到需要必须使用全局变量的场景,那么请保证一定要在全局变量使用完毕后将其设置为null
从而触发回收机制。
5.2 手动清除定时器
在我们的应用中经常会有使用setTimeout
或者setInterval
等定时器的场景,定时器本身是一个非常有用的功能,但是如果我们稍不注意,忘记在适当的时间手动清除定时器,那么很有可能就会导致内存泄漏,示例如下:
var numbers = [];
var fun = function() {
for(let i = 0;i < 100;i++) {
numbers.push(i);
}
};
window.setInterval(fun, 1000);
在这个示例中,由于我们没有手动清除定时器,导致回调任务会不断地执行下去,回调中所引用的numbers
变量也不会被垃圾回收,最终导致numbers
数组长度无限递增,从而引发内存泄漏。
5.3 少用闭包
闭包是JS中的一个高级特性,巧妙地利用闭包可以帮助我们实现很多高级功能。一般来说,我们在查找变量时,在本地作用域中查找不到就会沿着作用域链从内向外单向查找,但是闭包的特性可以让我们在外部作用域访问内部作用域中的变量,示例如下:
function foo() {
let local = 123;
return function() {
return local;
}
}
const bar = foo();
console.log(bar()); // -> 123
在这个示例中,foo
函数执行完毕后会返回一个匿名函数,该函数内部引用了foo
函数中的局部变量local
,并且通过变量bar
来引用这个匿名的函数定义,通过这种闭包的方式我们就可以在foo
函数的外部作用域中访问到它的局部变量local
。一般情况下,当foo
函数执行完毕后,它的作用域会被销毁,但是由于存在变量引用其返回的匿名函数,导致作用域无法得到释放,也就导致local
变量无法回收,只有当我们取消掉对匿名函数的引用才会进入垃圾回收阶段。
4.4 清除DOM引用
以往我们在操作DOM元素时,为了避免多次获取DOM元素,我们会将DOM元素存储在一个数据字典中,示例如下:
const elements = {
button: document.getElementById('button')
};
function removeButton() {
document.body.removeChild(document.getElementById('button'));
}
在这个示例中,我们想调用removeButton
方法来清除button
元素,但是由于在elements
字典中存在对button
元素的引用,所以即使我们通过removeChild
方法移除了button
元素,它其实还是依旧存储在内存中无法得到释放,只有我们手动清除对button
元素的引用才会被垃圾回收。
4.5 弱引用
在ES6中新增了两个有效的数据结构WeakMap
和WeakSet
,就是为了解决内存泄漏的问题而诞生的。其表示弱引用
,它的键名可以是对象,同时引用的对象均是弱引用,弱引用是指垃圾回收的过程中不会将键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存。
var map = new Map();
{
let x = {}
map.set(x, 'something');
}
console.log(map);
var map = new WeakMap();
{
let x = {}
map.set(x, 'something');
}
console.log(map);