内存由可读写单元组成,表示一片连续可操做的空间。在编程时,能够经过主动操做来申请,使用和释放可操做空间。内存管理指的就是主动操做过程,也就是申请内存,使用内存和释放内存。算法
// 申请内存 let str // 使用内存 str = 'foo' // 释放内存 str = null // 再也不引用,垃圾回收会自动回收内存
当内存再也不被使用时,其会被视为垃圾,而后被释放(回收)。编程
在JavaScript中,垃圾回收是自动进行的。
如何判断垃圾内存?数组
“根”在js中,能够将根看做全局对象。不能从根上访问到指的就是不能从全局对象上经过某条路径找到,能够是直接挂载在全局对象上,也能够是间接挂载在全局对象上。
function fn(obj1, obj2) { obj1['next'] = obj2 obj2['pre'] = obj1 return { o1: obj1, o2: obj2 } } const obj = fn()
上述代码的关系以下图所示,此时obj,obj1,obj2均可以从全局对象上找到,所以不能看成垃圾被回收。
以下图所示,若是经过delete将obj的o1属性和obj2的prev属性删除,那么obj1就没法从全局对象上找到,此时obj1将会被看成垃圾回收。浏览器
可到对象指的是能访问到的对象,访问的方式能够是引用,也能够是经过做用域链查找到。
判断一个对象是不是可达对象的标准就是从根出发是否能够被找到。缓存
GC能够理解为是垃圾回收机制的简写。算法也就指的是查找垃圾,回收垃圾的规则。
经常使用的GC算法包含如下几个:微信
经过引用计数器设置内存的引用数,当内存的引用关系发生改变的时候修改引用数,当引用数为0的时候内存当即被回收。闭包
// {name: 'zs'}所在的空间是一块内存 // 此时obj1引用这块内存,因此引用计数器上记为1 let obj1= {name: 'zs'} // obj2 一样引用了这块内存,因此引用计数器为2 let obj2 = obj1 // obj1 再也不引用这块内存,因此计数器变为1 obj1 = null // obj2也再也不引用这块内存,此时计数器为0.这块内存会被看成垃圾回收 obj2 = null
算法优势:app
算法缺点:dom
function fn() { const obj1 = { name: 'zs' } const obj2 = { name: 'ls' } // 在方法执行完毕之后,obj1和obj2应该被看成垃圾被回收,可是因为其相互引用,此时引用计数器上不为0, 因此没法回收 obj1['friend'] = obj2 obj2['friend'] = obj1 } fn()
标记清除算法将垃圾回收分为标记和删除阶段,其算法步骤以下:jsp
以下图所示,第一不找到全部活动对象,因为ABCDE能够经过全局对象找到,因此被标记,a1和b1不能经过全局对象找到,因此不会被标记。第二步,找到没有被标记的a1和b1,将其看成垃圾回收。
与引用计数算法相比。
优势:
缺点:
假设内存中有一段连续的内存空间ABCDEF,若是BCDE被标记为活动对象,AB和F没有被标记,那么AB,F会被看成垃圾回收。回收完成后,形成存在AB和F两个碎片内存能够被使用,其只能放入对应长度的数据。
标记整理算法和标记清除算法相似,只是多了整理内存步骤。
经过整理,能够解决标记清除算法形成内存碎片化的问题。
V8是一款主流的JavaScript执行引擎,采用即时编译,内存有限制(64位1.5G,32位800M)。
js中的数据分为原始数据和对象引用数据两种,其中原始数据是由语言自己去处理,因此此处的垃圾回收策略主要针对栈上的对象引用数据。
V8采起分代回收的策略,因为v8对内存大小有限制,因此其将内存分红新生代和老生代两种,不一样的生代采起不一样的垃圾回收策略。
V8主要采起的GC算法有以下:
V8将内存分为两块,其中小的空间称为新生代(64位32M/32位16M),其主要存储存活时间较短的对象。新生代内部一样分为两个等大小的空间From和To,经过空间复制和标记整理两个算法完成垃圾回收。
From到To的拷贝过程可能出发晋升,也就是重新生代拷贝到老生代,下面两种状况将出发晋升。
老生代指的是空间较大的内存块(64位1.4G,32位700M),其内部存储存活时间长的对象,采用标记清除,标记整理和增量标记三种算法实现垃圾回收。
js代码在浏览器中执行的时候,可能出现的和内存相关的问题以下:
全局变量会致使的问题以下:
将不可避免的全局变量缓存到局部做用域中,减小查找时间,优化性能。适用于在局部做用域中频繁使用某个全局变量。
function query() { // 在局部做用域中直接使用全局的document变量,在执行时,局部做用域找不到该变量,会沿着做用域链向上查找直到在全局中找到 return document.getElementsByTagName('input') } function query1() { // 经过将全局变量赋值给局部变量,那么查找时直接在局部做用域找到,不用再向上查找 let dom = document return dom.getElementsByTagName('input') }
在为全部的实例对象添加共享方法的时候,经过原型定义比在构造函数中经过this定义性能更好。这是因为构造函数中this定义的方法在每一个实例中都会保存一份单独的引用,而经过原型定义,全部的实例会指向同一个引用。
function Person() { // 每一个实例对象都会保存一份say的引用,10个就会有10个内存引用 this.say = function () { console.log(1) } } const zs = new Person() function Person1() { } // 全部实例的原型都指向一个内存引用,减小内存开销 Person1.prototype.say = function () { console.log(1) } const ls = new Person1()
闭包是指在外部做用域中可使用内部做用域中的变量。
function foo() { let str = 'foo' return function () { console.log(str) } } let f = foo() // f在外部执行的时候依然可以访问foo做用域中的str变量 f()
闭包是一种常见写法,能够解决js编程中的不少问题,可是因为内部做用域中的变量被外部引用,因此此变量不能被垃圾回收,若是使用不当很容易形成内存泄露,所以在编程中不能为了闭包而闭包。
js在编写类的时候,很容易的出如今类上提供一个方法,该方法用于访问类内部的一个属性。
function Person() { this.name = 'foo' // 为了便于控制,在属性的访问上添加了一层 this.getName = function () { return this.name } } const zs = new Person() console.log(zs.getName) function Person1() { this.name = 'foo' } const ls = new Person1() // 直接访问属性 console.log(ls.name)
经过jsperf测试,发现直接访问会比包装访问要快的多。所以抛开代码编写规范,单从执行速度上来说,直接访问更快。
let arr = Array(100).fill('foo') // 每次循环都要获取数组长度 for (let i = 0; i < arr.length; i++) { console.log(i) } // 缓存数组长度, for (let i = 0, len = arr.length; i < len; i++) { console.log(i) }
缓存数组长度for循环执行速度要更快,特别适合很是大或者很是复杂的数组遍历。
let arr = Array(100).fill('foo') arr.forEach(function (item) { console.log(item) }) for (let i = 0, len = arr.length; i < len; i++) { console.log(i) } for (let i in arr) { console.log(arr[i]) }
经过jsperf工具发现,forEach的执行速度最快,所以在不影响功能的前提下,尽可能使用forEach可加快代码的执行速度。
在日常的js代码编写过程当中,经常伴有Dom节点的添加,因为Dom节点添加操做经常伴有回流和重绘,这两个操做比较耗时,可使用文档碎片优化这种耗时操做。
for (let i = 0; i < 10; i++) { let p = document.createElement('p') document.body.append(p) } let fraEls = document.createDocumentFragment() for (let i = 0; i < 10; i++) { let p = document.createElement('p') fraEls.append(p) } document.body.append(fraEls)