前奏曲:了解闭包,先了解前两项
1、环境和作用域
话不多说,直接看代码:
...
<button>测试闭包</button>
...
...
let title = '鼓浪屿'
document.querySelector('button').addEventListener('click',()=>{
console.log(title);
})
function add(){
console.log(title)
}
add()
...
Tips:全局环境中的数据,如上:title和add不会被卸载掉(不被回收),除非人为的回收,比如:数据置空、关闭浏览器等。
function add() {
let num = 1;
console.log(++num);
}
add(); // 2
add(); // 2
Tips:调用两次函数,每调用一次函数,就生成不一样的内存地址,上文中的num的数据是不会共享的,即不会互相影响。
function name1() {
let num = 1;
console.log(++num);
}
let fn1 = name1 // 这里是函数名的改变,并没有开辟新的空间
console.log(name1, fn1); //都是:ƒ name1() {let num = 1; console.log(++num);}
console.log(fn1 === name1,'one'); // true 'one' ,表面指向同一个函数地址
fn1() // 2
fn1() // 2
function name2() {
let num = 1;
return function () {
console.log(++num);
};
}
let fn2 = name2(); //返回出去的函数储存在fn中,这个num变量一直在使用,所以不会被销毁
console.log(name2, fn2);// ƒ name2() { let num = 1; return function () { console.log(++num) }} 和 ƒ () {console.log(++num);}
console.log(fn2 === name2(),'two'); // false 'two' ,表面指向不同的函数地址
fn2(); // 2
fn2(); // 3
2、块级作用域和函数作用域
let和var都具备函数作用域:
// var是有函数作用域的,let也是有函数作用域
function add() {
a = 1; // 在window中声明可以取到
var b = 2
let c = 3
}
add();
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
不易被发现的函数作用域:
// 1.
let arr = []
for (var i = 0; i < 3; i++) {
// console.log(i);
arr.push(()=> i) // 函数是有函数作用域的
}
console.log(arr[0]()); // 3
// 2.
let brr = []
for (let i = 0; i < 3; i++) {
// console.log(i);
brr.push(()=> i) // 函数是有函数作用域的
}
console.log(brr[0]()); // 0
Tips:第一次用了var,不存在块级作用域,直接取得全局window的i;第二次用了let,存在块级作用域,直接取用块级作用域的数据。
// 问题现象,如下:
let arr = [1, 2, 3, 4];
for (var i = 0; i < arr.length; i++) {
setTimeout(() => {
console.log(i); // 4、4、4、4
}, 0);
}
// 解决方案1:let
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
console.log(i); // 0、1、2、3
}, 0);
}
// 解决方案2:用函数模拟出,let的“块”的行为
for (var i = 0; i < arr.length; i++) {
(function(j) {
setTimeout(() => {
console.log(j); // 0、1、2、3
}, 0);
})(i);
}
3.闭包特性及优缺点(本文的重点)
特性:
1、函数套函数;
2、内部函数可以直接使用外部函数的局部变量或参数;
3、变量或参数不会被垃圾回收机制回收 GC;
优点:
1、变量长期保存在内存中;
2、避免被全局变量污染;
3、私有成员的存在;
缺点:
1、由于闭包中的变量长期存储在内存中,或造成内存消耗,会造成内存泄漏;
2、引用的变量可能发生变化;
最简单的数据应用,如下:
// 如何取出数组中,任意区间的数据?
let arr = [1, 2, 3, 45, 50, 51, 90, 100, 600];
function fn(prev, next) { // fn函数的作用:为了传递prev和next的形参
return function(item) { // 这个匿名函数才是filter真正的回调函数
return item > prev && item < next;
};
}
let res = arr.filter(fn(50, 100));
console.log(res); // [ 51, 90 ]
稍微复杂点的数据应用,如下:
// 创建一个累积和数组
const accumulate = arr =>
arr.map(
(function(sum) {
return function(value) {
return sum += value;
};
})(0)
);
let res = accumulate([1, 2, 3, 4]); // [1, 3, 6, 10]
console.log(res);
内存泄露和垃圾回收
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
常见的内存泄露,如下:
- 错误使用全局变量
function fn() {
a = "gulangyu"; //a成为一个全局变量,不会被回收
this.b = 'gulangyu'// 函数自身发生调用,this指向全局对象window
}
// JS对未声明的变量都会挂载到全局对象上。只有在页面刷新或者关闭时才会释放内存。解决方案:使用let、const声明变量或者使用严格模式
- console方法的不删除
console.log(a) // 开发工具中可以查看这个信息,表明存在内存泄露
- 闭包中,返回的函数使用父级的变量
function add(name){
return function(){
return name
}
}
let res = add('gulangyu')
console.log(res()); // gulangyu
// 因为add()函数被全局变量res引用,所以在add()函数内部的匿名函数不会被回收,也就是内层函数私有作用域不会销毁,处于随时被调用的状态;
// 由于闭包会携带其函数作用域,所以比普通函数占用更大的内存,减少闭包的使用;
// 如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁.
** 值得一提的是:闭包不一定导致内存泄露!!! **
- dom泄露,元素存在变量中没释放
var a = document.getElementById('id');
document.body.removeChild(a);
// 虽然removeChild移除了a的dom元素,但由于存在变量a对它的引用,即DOM元素还在内存里面;
// 浏览器中的dom采用的是渲染引擎,而js采用的是v8引擎 1.浏览器的JS引擎与DOM引擎共享一个主线程。当调用DOM时,会将JS引擎挂起然后再启动DOM引擎。调用结束后,重启JS引擎继续执行。这种上下文的切换很耗性能。 2.很多调用DOM操作涉及重排和重绘,以确保返回值的准确,更消耗性能。
- 计时器,链式setTimeout和setInterval未使用clear清除计时器
// setInterval, 不用多说,不clearInterval就会持续性执行。
谷歌浏览器怎么直观的查看内存泄露的情况呢?
图解:
图片左上角①:Record,点击后会记录一个时间段,下方JS Heap会形成一个内存占用的梯形图
图片左上角②:Starting profiling and reload page(开始分析并重新加载页面),重载,其他同上
图片左上角③:Clear,清空
let a = 123
如下图:
let b = 1
setInterval(() => {
b++
}, 500);
如下图:
Tips:梯形图在一定时间内,不断的在上升,说明:一直有新的内存出现,即内存发生泄露了
// 代码段1
function add() {
let num = 100
return function () {
return num++
}
}
let res = add()
console.log(res()); // 100
如下图:
// 代码段2
function add() {
return function () {
return 100
}
}
let res = add()
console.log(res()); // 100
如下图:
Tips:可以看在上面的代码在浏览器中看到一个细节,把代码段1改成代码段2,梯形图并不会变成"水平线",只有当把浏览器关闭重新打开才能直接变成水平线,可见关闭浏览器可以强制清空内存的占用。
垃圾回收机制:
内存生命周期:
1.内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
2.内存使用:即读写内存,也就是使用变量、函数等
3.内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
原理:垃圾收集器会按照固定的时间间隔, 周期性的找出不再继续使用的变量,然后释放其占用的内存
策略:标记清除 和 引用计数