认识内存管理
- 不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存:
- 不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期:
- 第一步:分配申请你需要的内存(申请);
- 第二步:使用分配的内存(存放一些东西,比如对象等);
- 第三步:不需要使用时,对其进行释放;
- 不同的编程语言对于第一步和第三步会有不同的实现:
手动管理内存:比如C、C++ ,包括早期的OC,都是需要手动来管理内存的申请和释放的(malloc和free函
数);
自动管理内存:比如Java、JavaScript、Python、Swift、Dart等,它们有自动帮助我们管理内存;
JS的内存管理
JavaScript会在定义变量时为我们分配内存。
- JS对于基本数据类型内存的分配会在执行时,直接在栈空间进行分配;
- JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并旦将这块空间的地址给变量引用;
JS的垃圾回收
- 因为内存的大小是有限的,所以当内存不再需要的时候 ,我们需要对其进行释放,以便腾出更多的内存空间。
- 在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数:
但是这种管理的方式其实非常的低效 ,影响我们编写逻辑的代码的效率;
并县这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露; - 所以大部分现代的编程语言都是有自己的垃圾回收机制:
垃圾回收的英文是Garbage Collection , 简称GC;
对于那些不再使用的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;
而我们的语言运行环境,比如Java的运行环境IM,Javascript的运行环境js引擎都会内存 垃圾回收器;
垃圾回收器我们也会简称为GC,所以在很多地方你看到GC其实指的是垃圾回收器; - GC通过GC的算法知道哪些对象不再使用;常见的GC算法有两个:
- 引用计数
- 当一个对象有一个引用指向它时 ,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;
- 这个算法存在一个弊端就是会产生循环引用
- 标记清除
- 这个算法是设置一个根对象( rootobject),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象(如下图的M,N);
- 这个算法可以很好的解決循环引用的问题
- JS引擎比较广泛的采用的是标记清除算法,类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法。
JS函数
1. JS中函数是一等公民
那么就意味着函数的使用是非常灵活的。
- 它可以作为另外一个函数的参数;
function calc(num1, num2, calcFn){
console.log(calcFn(num1, num2))
}
// 加法
function add(num1, num2){
return num1 + num2
}
// 减法
function sub(num1, num2){
return num1 - num2
}
// 乘法
function mul(num1, num2){
return num1 * num2
}
var m = 20
var n = 10
calc(m, n, add) // 调用加法,打印30
calc(m, n, sub) // 调用减法,打印10
calc(m, n, mul) // 调用乘法,打印200
- 也可以作为另外一个函数的返回值来使用;
function makeAdder(count){
function add(num){
return count + num
}
return add
}
var add10 = makeAdder(10)
var add200 = makeAdder(200)
console.log(add10(20)) // 10 + 20 = 30
console.log(add10(30)) // 10 + 30 = 40
console.log(add200(20)) // 200 + 20 = 220
console.log(add200(30)) // 200 + 30 = 230
2. 高阶函数
如果一个函数接受另外一个函数作为参数(上面的calc函数),或者返回值是一个函数(上面的makeAdder函数),那么这个函数就称为高阶函数。
例如:数组中的函数使用
filter:过滤
var nums = [5, 10, 11, 100, 55]
var newNums = nums.filter(function(item){
return item % 2 === 0
})
console.log(newNums) // [10, 100]
map:映射
var nums = [5, 10]
var newNums = nums.map(function(item){
return item * 10
})
console.log(newNums) // [50, 100]
forEach:迭代(没有返回值;不能被return或break打断,除非抛异常;for循环可以被打断)
var nums = [5, 10, 11, 100, 55]
nums.forEach(function(item){
console.log(item)
})
find / findIndex:查找
var friends = [
{ name: "why", age: 18 },
{ name: "james", age: 30 },
{ name: "lucy", age: 20 },
]
var findFriend = friends.find(function(item){
return item.name === 'lucy'
})
console.log(findFriend) // { name: "lucy", age: 20 }
// findIndex:查找索引值
var friendIndex = friends.findIndex(function(item){
return item.name === 'lucy'
})
console.log(friendIndex) // 2
reduce:累加
var nums = [5, 10, 11, 100, 55]
var total = 0
// prevValue:上一次的值
var newNums = nums.reduce(function(prevValue,item){
// prevValue: 0,item:5
// prevValue: 5,item:10
// prevValue: 15,item:11
// prevValue: 26,item:100
// prevValue: 126,item:55
return preValue + item
}, 0) // 这个0代表第一次的prevValue默认值,不写的话默认也是0
console.log(total) // 181
闭包
由两部分组成:函数 + 可以访问的自由变量
JS中一个函数,如果访问了外层作用域的变量,那么它是一个闭包;
例如下面代码:
var name = "foo"
function bar() {
console.log(name) // 访问了外层作用域的变量
}
function foo() {
var name = "foo" // 可以访问的自由变量
function bar() { // 函数
console.log("bar", name) // 访问了外层作用域的变量
}
return bar
}
var fn = foo()
fn()
闭包的内存泄漏
function foo() {
var name = "foo"
var age = 18
function bar() {
console.log(name)
console.log(age)
}
return bar
}
var fn = foo()
fn()
执行过程解析:
var fn = foo()
这句代码 return 了bar函数,将bar赋值给fn,所以GO里面的 fn就指向了bar的内存地址0xb00(箭头3);
函数foo执行完之后,那foo的执行上下文就会从调用栈移除(下图第二个红框),按理说 foo函数对象(0xa00) 以及 foo的AO对象(0x200)都会同时被销毁,但是由于以下的引用关系,所以 0xa00 和 0x200 并不会销毁,而是继续存在内存中;
GO.foo -> 0xa00
GO.fn -> 0xb00 -> 0x200
执行fn()
就是调用bar()函数,输出name和age,由于bar的AO对象是空的(下图没有画),所以向父作用域 0x200 查找,找到并输出;执行完之后 bar的函数执行上下文就会从调用栈移除(下图第一个红框)。
总结:
从以上过程可以看出,使用闭包函数的时候有可能会造成循环引用,导致了foo和bar无法释放内存,这就造成了内存泄漏。
如果想要解决内存泄漏的问题,只需要将fn 和 foo 指向一个空地址(null)即可。当垃圾收集器下次运行时,就会回收它们占用的内存。
fn = null
foo = null